Compare commits

..
Author SHA1 Message Date
ZacharyZhang-NY 466c2ca14a Point package managers at China mirrors on cn installs
npm, PyPI and uv, Go, rustup and crates.io, RubyGems and Bundler,
and Maven reach their registries slowly or not at all from China.
On a cn install, link the profile's environment, cargo and gem
drop-ins into place. A regular file the admin wrote is never
replaced; the install fails instead.
2026-10-12 00:29:13 -04:00
Kros DaiandClaude Opus 5.5 ec4579a461 🐛 fix(test): skip exact zone match without tzdata.zi
- 🐛 minimal containers lack the link table, so the exact china zone check failed on a correct profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:44:15 -04:00
Kros DaiandClaude Opus 5.5 d9a781e3c9 📝 docs(region): scope the refresh render boundary
- 📝 state that same-user code is outside the refresh boundary, so reviewers see the trade-off was deliberate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 16:08:36 -04:00
Kros DaiandClaude Opus 5.5 a6c3480ff8 📝 docs(region): state marker mode and archlinuxcn risk
- 📝 refresh reads the marker unprivileged, and rolling archlinuxcn packages can outpace stable snapshots

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 545190596d ♻️ refactor(pacman): branch on render with if/else
- ♻️ follow the repo style instead of exiting early to skip the install block

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 b381093e32 ✅ test(region): derive china zones from tzdata
- ✅ dropping a backward-compatible zone name from the profile now fails the test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 9d74c813f1 🐛 fix(channel): require every region fragment for dev
- 🐛 leaving dev refreshes stable or rc with the checkout's tools, so a profile missing those fragments would drop regional repositories
- ✅ refuse a profile missing any single fragment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:31:04 -04:00
Kros DaiandClaude Opus 5.5 fce85abf48 📝 docs(region): explain mirror order and fallback
- 📝 pacman's three-error skip and first-server databases decide what the china replicas must serve

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 3c68a1bb04 ✨ feat(region): put the china channel mirrors first
- ✨ omarchy now runs per-channel china replicas, so they should lead instead of trailing as a fallback
- ♻️ prepend per-channel fragments so each channel's own mirror stays the fallback
- 🔥 drop the ustc arch mirror, which was never a stable or rc snapshot replica

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:21:51 -04:00
Kros DaiandClaude Opus 5.5 b89def6c7f 📝 docs(region): describe timezone-derived regions
- 📝 the --region build flag is gone, so the installer contract and validation plan change with it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:46 -04:00
Kros DaiandClaude Opus 5.5 0de49b231a ✨ feat(region): map timezones to regions
- ✨ one iso serves every region, so the installer derives it from the timezone the user already picks
- ✅ reject unknown zones and zones claimed by two regions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuATgjU4mnqpqSLAKMQ14g
2026-10-08 15:03:41 -04:00
Kros DaiandClaude Opus 5.5 3e00a48c00 ✅ test(pacman): sandbox the region for every boundary test
- ✅ The channel and update boundary tests copied the real render and dev-region guard, so on a China install they read the host's `/etc/omarchy/region` and failed before reaching their checks
- ♻️ The shared fixture now points both lookups at a sandbox target that starts global, and the regional test only writes `cn` there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:14:56 -04:00
Kros DaiandClaude Opus 5.5 a1bdc46119 ✅ test(pacman): isolate the regional refresh boundary test
- ✅ the render read the host's region marker, so a global machine never rendered China and a China machine failed on missing fragments
- ✅ check the finished China files root installs, not only the copy order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 7a60c8dea4 🐛 fix(channel): require a usable region profile for dev
- 🐛 an empty profile directory or a helper that cannot run passed the guard, linked dev, and then broke the refresh
- ✅ the success case now uses a complete profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 22:00:59 -04:00
Kros DaiandClaude Opus 5.5 6be88d14a5 🐛 fix(pacman): remove the render dir on every refresh exit
- 🐛 a failed render, a refused sudo prompt, or a signal left the scratch directory behind; repeated attempts piled them up
- 🔒 the cleanup runs before the sudo revocation, which stays the last step of the exit path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdXqTwm63rRMwotZ1xqLC8
2026-10-06 21:58:48 -04:00
Kros DaiandClaude Opus 5.5 50a26be6a7 📝 docs(region): scope the cn profile to repositories
- 📝 frame USTC and ArchLinuxCN as a bridge until an official China mirror exists
- 📝 record that language, timezone, and input defaults follow as separate changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d5ef1c1d15 🔥 refactor(region): drop input defaults from the cn profile
- 🔥 keep this change to the regional build architecture; Chinese input gets its own proposal instead of fixing one engine here
- ✅ assert the shipped profile adds only its repository keyring

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d024c82a9d 📝 docs(plans): move the china proposal to a discussion
- 📝 upstream plans are implementation plans; the open questions belong in the discussion that links these changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3Jna1ogUUviYLJzhsG6Vr
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 d7f17a0424 ♻️ refactor(pacman): render regional config without root
- 🔒 upstream refresh now gives root only command-scoped copies, so regional files are rendered as the caller and installed with the same four `sudo cp` calls
- ✅ exercise refresh only inside the sudo-boundary sandbox; the old host-level test reached real sudo once refresh pinned its PATH

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 87a2931209 📝 docs(region): drop the unreachable menu input gate
- 📝 the menu bypasses the input method, so requiring Pinyin there made the release procedure impossible until upstream fixes it
- 📝 record the required `skel` profile and the dev-switch region check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 647979bde7 🐛 fix(pacman): keep existing config file modes
- 🐛 refresh used to copy over the existing files, so a customized mode must survive the shared regional applicator too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros DaiandClaude Opus 5.5 123535ea14 🐛 fix(channel): keep regional repos when switching to dev
- 🛡️ refuse a dev checkout that lacks this machine's region profile before linking it, since its own refresh would silently replace the regional pacman files
- ✅ isolate channel tests from the host region marker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 14:43:25 -04:00
Kros Dai 645cfc2a36 📝 docs(region): explain china bootstrap choices
- 📝 clarify optional defaults, trust requirements, and opt-out behavior
- ⚠️ document snapshot compatibility and release validation limits

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai e989960895 ✅ test(region): guard runtime defaults
- ✅ protect global behavior and regional mirror ordering
- ⌨️ guard personal settings and reserved input shortcuts

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros Dai 48f7c411c0 ✨ feat(pacman): preserve regional defaults
- ✨ share regional repository generation across install and refresh
- 🛡️ reject invalid profiles before replacing configuration

Amp-Thread-ID: T-01a0df38-0ea7-759b-bcff-29da223cc567
2026-10-06 14:43:25 -04:00
Kros DaiandAmp 7727d40c92 ✨ feat(region): seed offline chinese input
- ✨ make first-login pinyin available without downloads
- ⌨️ preserve compose and terminal prefix shortcuts

Amp-Thread-ID: https://ampcode.com/threads/T-01a0df38-0ea7-759b-bcff-29da223cc567
Co-authored-by: Amp <amp@ampcode.com>
2026-10-06 14:43:25 -04:00
68 changed files with 754 additions and 1167 deletions

No files matched your search

+5
View File
@@ -96,6 +96,11 @@ Exceptions are allowed for migration and package-helper scripts where the helper
- Do not add `aliases` to new menu entries. Aliases are reserved for - Do not add `aliases` to new menu entries. Aliases are reserved for
established alternate names users already type, kept for compatibility. established alternate names users already type, kept for compatibility.
# Region
- `/etc/omarchy/region` holds the region the installer derived from the timezone (`cn`); no file means Global. The profile lives in `default/regions/cn/`, its contract in `docs/regions.md`.
- `install/config/region.sh` links the `cn` developer mirrors into `/etc` and `/.cargo` and refuses to replace a regular file it did not create.
# Config Structure # Config Structure
- `config/` - default configs copied to `~/.config/` - `config/` - default configs copied to `~/.config/`
+8
View File
@@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Name=Disk Usage
Exec=xdg-terminal-exec --app-id=TUI.float -e bash -c "dua i /"
Terminal=false
Type=Application
Icon=disk-usage
StartupNotify=true
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# omarchy:summary=Restore channel and regional pacman defaults in an installed target
# omarchy:args=<stable|rc|edge> [target-root] | --render <stable|rc|edge> <output-dir> [target-root]
# omarchy:requires-sudo=true
# omarchy:hidden=true
set -euo pipefail
usage() {
echo "Usage: omarchy-apply-pacman <stable|rc|edge> [target-root]" >&2
echo " omarchy-apply-pacman --render <stable|rc|edge> <output-dir> [target-root]" >&2
exit 1
}
# --render only builds the two files, so an unprivileged caller can hand root
# nothing but plain copies of the finished result.
render_dir=""
if [[ ${1:-} == "--render" ]]; then
(( $# == 3 || $# == 4 )) || usage
channel="$2"
render_dir="$3"
[[ -d $render_dir ]] || { echo "Render directory not found: $render_dir" >&2; exit 1; }
etc="${4:-/}/etc"
else
(( $# >= 1 && $# <= 2 )) || usage
channel="$1"
etc="${2:-/}/etc"
fi
case "$channel" in
stable|rc|edge) ;;
*) echo "Invalid pacman channel: $channel" >&2; exit 1 ;;
esac
# The ISO writes this plain-text marker before target finalization. Missing
# markers keep existing installations global; never source it as shell code.
region=global
if [[ -e $etc/omarchy/region ]]; then
region=$(<"$etc/omarchy/region")
fi
case "$region" in
global|cn) ;;
*) echo "Unsupported region: $region (expected global or cn)" >&2; exit 1 ;;
esac
# Build both files before replacing either. Explicit refreshes retain backups;
# ordinary updates do not call this command or enforce regional defaults.
if [[ -n $render_dir ]]; then
staging="$render_dir"
else
staging=$(mktemp -d)
trap 'rm -rf "$staging"' EXIT
fi
cp "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" "$staging/pacman.conf"
if [[ $region == "global" ]]; then
cp "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" "$staging/mirrorlist"
else
profile="$OMARCHY_PATH/default/regions/$region/pacman"
printf '\n' >> "$staging/pacman.conf"
cat "$profile/pacman.conf.append" >> "$staging/pacman.conf"
# pacman tries servers in order, so the regional mirror leads and the
# channel's own mirror stays as the fallback.
cat "$profile/mirrorlist-$channel.prepend" "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" > "$staging/mirrorlist"
fi
if [[ -z $render_dir ]]; then
mkdir -p "$etc/pacman.d"
for file in pacman.conf pacman.d/mirrorlist; do
if [[ -e $etc/$file ]]; then
cp -f "$etc/$file" "$etc/$file.bak"
fi
done
# Copying over an existing file keeps its owner and mode, as refresh always has.
cp -f "$staging/pacman.conf" "$etc/pacman.conf"
cp -f "$staging/mirrorlist" "$etc/pacman.d/mirrorlist"
fi
+22
View File
@@ -47,9 +47,31 @@ validate_dev_checkout() {
fi fi
} }
# The dev channel refreshes pacman with the checkout's own tools. A checkout
# without this machine's region profile would silently drop its repositories.
validate_dev_region() {
local checkout="$1" region_file="${OMARCHY_REGION_FILE:-/etc/omarchy/region}" region=global
[[ -f $region_file ]] && region=$(<"$region_file")
[[ $region == "global" ]] && return 0
# The refresh after linking needs a runnable helper and every fragment, not
# just a directory with the region's name: dev refreshes edge, and leaving
# dev later refreshes stable or rc with this checkout's tools.
local profile="$checkout/default/regions/$region/pacman" fragment supported=true
[[ -x $checkout/bin/omarchy-apply-pacman ]] || supported=false
for fragment in pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend; do
[[ -r $profile/$fragment ]] || supported=false
done
if ! $supported; then
fail "$checkout does not support the '$region' region, so switching would drop its package repositories. Check out a branch with default/regions/$region, or remove $region_file to use global defaults."
fi
}
link_dev_checkout() { link_dev_checkout() {
local checkout="$1" required local checkout="$1" required
[[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout" [[ -d $checkout/.git ]] || git clone https://github.com/omacom/omarchy.git "$checkout"
validate_dev_region "$checkout"
# Check the destination before changing /etc/omarchy.conf or sudo's path. # Check the destination before changing /etc/omarchy.conf or sudo's path.
# An existing checkout is not pulled automatically and may predate this policy. # An existing checkout is not pulled automatically and may predate this policy.
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# omarchy:summary=Match the Dell XPS 13 DX13260 with an Intel Panther Lake GPU.
omarchy-hw-match "DX13260" && omarchy-hw-intel-ptl
+16 -2
View File
@@ -30,11 +30,25 @@ if [[ $channel != "stable" && $channel != "rc" && $channel != "edge" ]]; then
exit 2 exit 2
fi fi
# Render the channel templates plus this machine's regional additions as the
# caller, so root only copies finished files. The scratch directory goes on
# every exit, including a failed render, a refused sudo prompt, or a signal;
# revoking the sudo timestamp stays the last thing the exit path does.
rendered=$(mktemp -d)
cleanup_refresh() {
local status=$?
rm -rf "$rendered"
omarchy_security_exit_with_revoked_sudo "$status"
}
trap cleanup_refresh EXIT
"$OMARCHY_PATH/bin/omarchy-apply-pacman" --render "$channel" "$rendered"
sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak sudo cp -f /etc/pacman.conf /etc/pacman.conf.bak
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
echo "Setting channel to $channel" echo "Setting channel to $channel"
sudo cp -f "$OMARCHY_PATH/default/pacman/pacman-$channel.conf" /etc/pacman.conf sudo cp -f "$rendered/pacman.conf" /etc/pacman.conf
sudo cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-$channel" /etc/pacman.d/mirrorlist sudo cp -f "$rendered/mirrorlist" /etc/pacman.d/mirrorlist
rm -rf "$rendered"
# Allow user customization of /etc/pacman.conf before the upgrade runs, so # Allow user customization of /etc/pacman.conf before the upgrade runs, so
# custom repositories and IgnorePkg entries shape the downgrade-capable # custom repositories and IgnorePkg entries shape the downgrade-capable
+47 -194
View File
@@ -1,7 +1,7 @@
#!/bin/bash -p #!/bin/bash -p
# omarchy:summary=Toggle passwordless sudo for the current user. # omarchy:summary=Toggle passwordless sudo for the current user.
# omarchy:args=[MINUTES|permanent|--active|--disable] # omarchy:args=[MINUTES]
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then if [[ $- != *p* && ${BASH_SOURCE[0]} == "$0" ]]; then
@@ -22,6 +22,7 @@ fi
set -euo pipefail set -euo pipefail
readonly DEFAULT_MINUTES=15
readonly MAX_MINUTES=1440 readonly MAX_MINUTES=1440
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
@@ -33,7 +34,7 @@ readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
readonly STATUS_INACTIVE=3 readonly STATUS_INACTIVE=3
usage() { usage() {
echo "Usage: omarchy-sudo-passwordless [MINUTES|permanent|--active|--disable]" >&2 echo "Usage: omarchy-sudo-passwordless [MINUTES]" >&2
echo "MINUTES must be between 1 and $MAX_MINUTES." >&2 echo "MINUTES must be between 1 and $MAX_MINUTES." >&2
exit 1 exit 1
} }
@@ -42,10 +43,6 @@ valid_minutes() {
[[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES)) [[ $1 =~ ^0*[1-9][0-9]{0,3}$ ]] && ((10#$1 <= MAX_MINUTES))
} }
valid_duration() {
[[ $1 == "permanent" ]] || valid_minutes "$1"
}
valid_uid() { valid_uid() {
[[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294)) [[ $1 =~ ^0*[1-9][0-9]{0,9}$ ]] && ((10#$1 <= 4294967294))
} }
@@ -99,39 +96,14 @@ with_root_lock() {
} }
rule_file() { rule_file() {
local uid=$((10#$1)) printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$1"
if [[ ${2:-} == "permanent" ]]; then
printf '/etc/sudoers.d/99-omarchy-permanent-nopasswd-%s' "$uid"
else
printf '/etc/sudoers.d/99-omarchy-nopasswd-%s' "$uid"
fi
} }
# The sudoers rule is the only grant record. A missing file is distinct from # The sudoers rule is the only grant record. A missing file is distinct from
# an unreadable, unsafe, or administrator-modified file. # an unreadable, unsafe, or administrator-modified file.
read_grant() { read_grant() {
local file permanent contents local file contents
file=$(rule_file "$1") file=$(rule_file "$1")
permanent=$(rule_file "$1" permanent)
if [[ -e $permanent || -L $permanent ]]; then
verify_root_path "$permanent" && [[ -f $permanent ]] || return 2
contents=$(/usr/bin/cat -- "$permanent") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOPASSWD:\ ALL$ ]] || return 2
GRANT_NAME=${BASH_REMATCH[1]}
GRANT_DEADLINE=""
valid_account_name "$GRANT_NAME" || return 2
if [[ -e $file || -L $file ]]; then
# A SIGKILL between publication and removal can leave both policies.
# Only accept a matching generated timed rule. Permanent access already
# exists, so old expiry callbacks must not revoke it; disable removes
# both files, and the next duration change finishes their replacement.
verify_root_path "$file" && [[ -f $file ]] || return 2
contents=$(/usr/bin/cat -- "$file") || return 2
[[ $contents =~ ^([a-z_][a-z0-9_-]*\$?)\ ALL=\(ALL\)\ NOTAFTER=([0-9]{14}Z)\ NOPASSWD:\ ALL$ ]] || return 2
[[ ${BASH_REMATCH[1]} == "$GRANT_NAME" ]] || return 2
fi
return 0
fi
[[ -e $file || -L $file ]] || return "$STATUS_INACTIVE" [[ -e $file || -L $file ]] || return "$STATUS_INACTIVE"
verify_root_path "$file" && [[ -f $file ]] || return 2 verify_root_path "$file" && [[ -f $file ]] || return 2
contents=$(/usr/bin/cat -- "$file") || return 2 contents=$(/usr/bin/cat -- "$file") || return 2
@@ -149,9 +121,6 @@ classify_generated_rule() {
[[ -f $file && ! -L $file ]] || return 1 [[ -f $file && ! -L $file ]] || return 1
contents=$(/usr/bin/cat -- "$file") || return 2 contents=$(/usr/bin/cat -- "$file") || return 2
suffix=${file##*/99-omarchy-nopasswd-} suffix=${file##*/99-omarchy-nopasswd-}
if [[ $file == */99-omarchy-permanent-nopasswd-* ]]; then
suffix=${file##*/99-omarchy-permanent-nopasswd-}
fi
# The legacy command wrote the caller's unvalidated name into both the # The legacy command wrote the caller's unvalidated name into both the
# filename and the rule. That exact relationship is its fingerprint, so an # filename and the rule. That exact relationship is its fingerprint, so an
@@ -169,21 +138,14 @@ classify_generated_rule() {
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]] valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
} }
cleanup_rule_file() { cleanup_uid_locked() {
local file=$1 local file
file=$(rule_file "$1")
[[ -e $file || -L $file ]] || return 0 [[ -e $file || -L $file ]] || return 0
verify_root_path "$file" && classify_generated_rule "$file" || return 1 verify_root_path "$file" && classify_generated_rule "$file" || return 1
/usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]] /usr/bin/rm -f -- "$file" && [[ ! -e $file && ! -L $file ]]
} }
cleanup_uid_locked() {
local file duration
for duration in temporary permanent; do
file=$(rule_file "$1" "$duration")
cleanup_rule_file "$file" || return 1
done
}
# The generated prefix is reserved: boot cleanup and the package hook already # The generated prefix is reserved: boot cleanup and the package hook already
# remove everything in it, and the legacy writer could produce a rule whose # remove everything in it, and the legacy writer could produce a rule whose
# body differs from its filename. Nothing unrecognized may stay live there, but # body differs from its filename. Nothing unrecognized may stay live there, but
@@ -301,7 +263,6 @@ migrate_locked() {
expire_locked() { expire_locked() {
local status now local status now
if read_grant "$1"; then if read_grant "$1"; then
[[ -n $GRANT_DEADLINE ]] || return 0
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2 now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
[[ $now < $GRANT_DEADLINE ]] && return 0 [[ $now < $GRANT_DEADLINE ]] && return 0
cleanup_uid_locked "$1" cleanup_uid_locked "$1"
@@ -320,7 +281,6 @@ status_locked() {
resolve_account "$1" || return 2 resolve_account "$1" || return 2
if read_grant "$1"; then if read_grant "$1"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2 [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 2
[[ -n $GRANT_DEADLINE ]] || return 0
now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2 now=$(/usr/bin/date -u +%Y%m%d%H%M%SZ) || return 2
if [[ $now < $GRANT_DEADLINE ]]; then if [[ $now < $GRANT_DEADLINE ]]; then
return 0 return 0
@@ -340,11 +300,7 @@ finish_enable() {
if cleanup_uid_locked "$uid"; then if cleanup_uid_locked "$uid"; then
[[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true [[ -z $timer ]] || /usr/bin/systemctl stop "$timer.timer" "$timer.service" >/dev/null 2>&1 || true
else else
if [[ -n $timer ]]; then echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
echo "Could not revoke passwordless sudo; expiry remains armed. Administrator cleanup is required." >&2
else
echo "Could not revoke passwordless sudo. Administrator cleanup is required." >&2
fi
fi fi
fi fi
[[ -z $pending ]] || /usr/bin/rm -f -- "$pending" [[ -z $pending ]] || /usr/bin/rm -f -- "$pending"
@@ -353,15 +309,9 @@ finish_enable() {
enable_locked() ( enable_locked() (
local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status local uid=$1 minutes=$2 now expires deadline token timer="" pending="" file status
resolve_account "$uid" && valid_duration "$minutes" || return 1 resolve_account "$uid" && valid_minutes "$minutes" || return 1
verify_root_path /etc/sudoers.d || return 1 verify_boot_cleanup && verify_root_path /etc/sudoers.d || return 1
if [[ $minutes != "permanent" ]]; then file=$(rule_file "$uid")
verify_boot_cleanup || {
echo "Temporary sudo cleanup is unavailable or a settings package transaction is incomplete. Check the omarchy-settings installation." >&2
return 1
}
fi
file=$(rule_file "$uid" "$minutes")
if read_grant "$uid"; then if read_grant "$uid"; then
[[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1 [[ $GRANT_NAME == "$ACCOUNT_NAME" ]] || return 1
else else
@@ -370,69 +320,39 @@ enable_locked() (
fi fi
trap finish_enable EXIT trap finish_enable EXIT
omarchy_security_install_signal_exit_traps omarchy_security_install_signal_exit_traps
now=$(/usr/bin/date +%s) || return 1
expires=$((now + 10#$minutes * 60))
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1 pending=$(/usr/bin/mktemp /etc/sudoers.d/.omarchy-nopasswd.XXXXXX) || return 1
if [[ $minutes == "permanent" ]]; then /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
/usr/bin/printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$ACCOUNT_NAME" >"$pending" || return 1
else
now=$(/usr/bin/date +%s) || return 1
expires=$((now + 10#$minutes * 60))
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
/usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$ACCOUNT_NAME" "$deadline" >"$pending" || return 1
fi
/usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1 /usr/bin/chown root:root "$pending" && /usr/bin/chmod 0440 "$pending" || return 1
/usr/sbin/visudo -cf "$pending" >/dev/null || return 1 /usr/sbin/visudo -cf "$pending" >/dev/null || return 1
if [[ $minutes != "permanent" ]]; then token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1
token=$(/usr/bin/tr -d '-' </proc/sys/kernel/random/uuid) || return 1 [[ $token =~ ^[0-9a-f]{32}$ ]] || return 1
[[ $token =~ ^[0-9a-f]{32}$ ]] || return 1 timer="omarchy-nopasswd-expire-$uid-$token"
timer="omarchy-nopasswd-expire-$uid-$token" /usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \
/usr/bin/systemd-run --quiet --collect --on-calendar="@$expires" \ --timer-property=AccuracySec=1s --unit="$timer" \
--timer-property=AccuracySec=1s --unit="$timer" \ -- "$INSTALLED_SELF" __expire "$uid" || return 1
-- "$INSTALLED_SELF" __expire "$uid" || return 1 /usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
/usr/bin/systemctl is-active --quiet "$timer.timer" || return 1
fi
# The temporary filename contains a dot, so sudo ignores it. Rename within # The temporary filename contains a dot, so sudo ignores it. Rename within
# sudoers.d publishes the complete validated policy in one operation. # sudoers.d publishes the complete validated policy in one operation.
/usr/bin/mv -fT -- "$pending" "$file" || return 1 /usr/bin/mv -fT -- "$pending" "$file" || return 1
pending="" pending=""
# Keep access uninterrupted during renewal. When changing duration type, now=$(/usr/bin/date +%s) || return 1
# remove the other rule only after its replacement is published. The lock (( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
# keeps status and expiry callbacks from observing both rules together.
if [[ $minutes == "permanent" ]]; then
cleanup_rule_file "$(rule_file "$uid")" || return 1
else
cleanup_rule_file "$(rule_file "$uid" permanent)" || return 1
fi
if [[ $minutes != "permanent" ]]; then
now=$(/usr/bin/date +%s) || return 1
(( now < expires )) && verify_boot_cleanup && /usr/bin/systemctl is-active --quiet "$timer.timer"
fi
) )
root_dispatch() { root_dispatch() {
local action="$1" status local action="$1"
shift shift
case "$action" in case "$action" in
__status) __status)
(($# == 1)) && verify_sudo_caller "$1" || return 2 (($# == 1)) && verify_sudo_caller "$1" || return 2
if with_root_lock status_locked "$1"; then with_root_lock status_locked "$1"
return 0
else
status=$?
# Reserve status 1 for sudo refusing noninteractive authentication.
# Every internal inspection failure, including lock failures, is 2.
(( status == STATUS_INACTIVE )) || status=2
return "$status"
fi
;; ;;
__enable) __enable)
(($# == 2)) && verify_sudo_caller "$1" && valid_duration "$2" || return 1 (($# == 2)) && verify_sudo_caller "$1" && valid_minutes "$2" || return 1
if with_root_lock enable_locked "$1" "$2"; then with_root_lock enable_locked "$1" "$2"
return 0
else
status=$?
echo "Could not enable passwordless sudo. Check existing sudoers policy and cleanup support." >&2
return "$status"
fi
;; ;;
__disable) __disable)
(($# == 1)) && verify_sudo_caller "$1" || return 1 (($# == 1)) && verify_sudo_caller "$1" || return 1
@@ -462,14 +382,6 @@ root_dispatch() {
esac esac
} }
show_enabled() {
if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else
echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
fi
}
case "${1:-}" in case "${1:-}" in
__status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete) __status|__enable|__disable|__expire|__cleanup-all|__package-removing|__migrate|__migration-complete)
action=$1 action=$1
@@ -479,30 +391,9 @@ case "${1:-}" in
;; ;;
esac esac
# Prefer matched policy tags to executing a privileged command, avoiding
# authentication/session logs under the default listing policy. Restrictive
# listpw settings require the root-status fallback. Neither path prompts or
# trusts cached credentials as evidence of passwordless access.
if [[ ${1:-} == "--active" ]]; then
(($# == 1)) || usage
uid=$(/usr/bin/id -u)
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then
/usr/bin/grep -q '!authenticate' <<<"$policy"
else
# listpw=always can require authentication to list a passwordless grant.
# With a command, -k ignores cached credentials without invalidating them.
status=0
/usr/bin/sudo -kn -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
# An internal inspection error still proves that sudo ran the helper
# without authentication. Keep the warning visible for unsafe policy.
(( status == 0 || status == 2 )) || exit 1
fi
exit
fi
(($# <= 1)) || usage (($# <= 1)) || usage
minutes=${1:-} minutes=${1:-$DEFAULT_MINUTES}
(($# == 0)) || [[ $minutes == "--disable" ]] || valid_duration "$minutes" || usage valid_minutes "$minutes" || usage
uid=$(/usr/bin/id -u) uid=$(/usr/bin/id -u)
valid_uid "$uid" || { valid_uid "$uid" || {
echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2 echo "omarchy-sudo-passwordless: cannot grant passwordless sudo to this account" >&2
@@ -520,76 +411,38 @@ omarchy_security_install_sudo_cleanup_traps
exit 1 exit 1
} }
# Inspect policy without authenticating. Only inspect the root-owned grant echo "Toggle passwordless sudo..."
# when policy already permits it without a password; otherwise the eventual if /usr/bin/sudo -N -- "$INSTALLED_SELF" __status "$uid"; then
# enable action is the sole authentication boundary. enable_locked validates if (($# == 0)); then
# any existing policy before publishing, including expired or modified rules. /usr/bin/sudo -N -- "$INSTALLED_SELF" __disable "$uid"
status=$STATUS_INACTIVE echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
if policy=$(/usr/bin/sudo -n -N -l -l -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null); then else
if /usr/bin/grep -q '!authenticate' <<<"$policy"; then /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
status=0 echo "Passwordless sudo expiry updated. It will automatically disable in ${minutes} minutes."
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" || status=$?
fi fi
else else
# A listing password is independent of command authorization. An active status=$?
# grant can still be inspected/revoked without prompting under listpw=always. if (( status != STATUS_INACTIVE )); then
status=0
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __status "$uid" 2>/dev/null || status=$?
if (( status == 1 )) && [[ $minutes != "--disable" ]]; then
# No noninteractive inspection is available. Offer the confirmed enable
# flow; its single authenticated action validates existing policy itself.
status=$STATUS_INACTIVE
fi
fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
show_enabled
else
if (( status != 0 && status != STATUS_INACTIVE )); then
echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2 echo "Could not safely inspect passwordless sudo; no grant will be enabled. Resolve the reported authorization or cleanup error first." >&2
exit 1 exit 1
fi fi
if [[ -z $minutes ]]; then
choice=$(/usr/bin/gum choose --header "How long should passwordless sudo stay enabled?" "15 minutes" "1 Hour" "1 Day" "Permanently") || exit 130
case "$choice" in
"15 minutes") minutes=15 ;;
"1 Hour") minutes=60 ;;
"1 Day") minutes=1440 ;;
"Permanently") minutes=permanent ;;
*) exit 130 ;;
esac
fi
if [[ $minutes == "permanent" ]]; then
duration="permanently"
else
duration="for ${minutes} minutes"
fi
echo "" echo ""
echo "⚠️ WARNING: This will allow ANY process running as your user to" echo "⚠️ WARNING: This will allow ANY process running as your user to"
echo "execute ANY command as root WITHOUT a password $duration." echo "execute ANY command as root WITHOUT a password for ${minutes} minutes."
echo "" echo ""
echo "This is useful for AI agents that need to run sudo commands," echo "This is useful for AI agents that need to run sudo commands,"
echo "but it significantly weakens the security of your system." echo "but it significantly weakens the security of your system."
echo "Anyone or anything with access to your user account gets full root." echo "Anyone or anything with access to your user account gets full root."
echo "" echo ""
if [[ $minutes == "permanent" ]]; then echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "Passwordless sudo will remain enabled across reboots until you disable it." echo "including if the machine reboots before the deadline."
else echo "Run this command again to disable it early."
echo "Passwordless sudo will automatically disable after ${minutes} minutes,"
echo "including if the machine reboots before the deadline."
fi
echo "Run this command again to disable it."
echo "" echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then if /usr/bin/gum confirm "Enable passwordless sudo for ${minutes} minutes? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes" /usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo "" echo ""
show_enabled echo "Passwordless sudo has been ENABLED. It will automatically disable in ${minutes} minutes."
else else
echo "Aborted. No changes made." echo "Aborted. No changes made."
fi fi
+1 -1
View File
@@ -127,7 +127,7 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
omarchy-update-system-pkgs omarchy-update-system-pkgs
omarchy-migrate omarchy-migrate
omarchy-update-orphan-pkgs -y omarchy-update-orphan-pkgs
omarchy-update-analyze-logs omarchy-update-analyze-logs
omarchy-update-status omarchy-update-status
+3 -5
View File
@@ -1,8 +1,6 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Review and optionally remove orphaned system packages after updates # omarchy:summary=Review and optionally remove orphaned system packages after updates
# omarchy:args=[-y]
# omarchy:examples=omarchy update orphan-pkgs | omarchy update orphan-pkgs -y
# omarchy:requires-sudo=true # omarchy:requires-sudo=true
set -e set -e
@@ -14,18 +12,18 @@ echo -e "\e[32m\nOrphan system packages\e[0m"
printf ' %s\n' "${orphans[@]}" printf ' %s\n' "${orphans[@]}"
echo echo
if [[ ${1:-} != "-y" && ( ! -t 0 || ! -t 1 ) ]]; then if [[ ! -t 0 || ! -t 1 ]]; then
echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them." echo "${#orphans[@]} orphaned package(s) found. Re-run omarchy-update-orphan-pkgs in a terminal to review/remove them."
echo echo
exit 0 exit 0
fi fi
if [[ ${1:-} != "-y" ]] && ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then if ! gum confirm --default=false "Remove ${#orphans[@]} orphaned package(s)?"; then
echo "Keeping orphaned packages." echo "Keeping orphaned packages."
echo echo
exit 0 exit 0
fi fi
echo -e "\e[32m\nRemoving orphan system packages\e[0m" echo -e "\e[32m\nRemoving orphan system packages\e[0m"
omarchy-pkg-drop "${orphans[@]}" sudo pacman -Rns "${orphans[@]}"
echo echo
+1 -1
View File
@@ -1,5 +1,5 @@
--ozone-platform=wayland --ozone-platform=wayland
--ozone-platform-hint=wayland --ozone-platform-hint=wayland
--password-store=gnome-libsecret --password-store=gnome-libsecret
--enable-features=TouchpadOverscrollHistoryNavigation,OverlayScrollbar --enable-features=TouchpadOverscrollHistoryNavigation
--load-extension=/usr/share/omarchy/default/chromium/extensions/copy-url,/usr/share/omarchy/default/chromium/extensions/yt-dlp,/usr/share/omarchy/default/chromium/extensions/whatsapp-slim --load-extension=/usr/share/omarchy/default/chromium/extensions/copy-url,/usr/share/omarchy/default/chromium/extensions/yt-dlp,/usr/share/omarchy/default/chromium/extensions/whatsapp-slim
+4 -3
View File
@@ -6,9 +6,10 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
-- Launcher, image selector, emojis, clipboard overlays, the OSD, reminders, the -- Launcher, image selector, emojis, clipboard overlays, the OSD, reminders, the
-- Wi-Fi QR code and keyboard-driven panels should pop without compositor layer -- Wi-Fi QR code and keyboard-driven panels should pop without compositor layer
-- animations. Overlays open as soon as their scale is ready, without a -- animations. The overlays stay mapped between opens and grow from a parked
-- compositor fade or slide. Panels keep their own QML opacity transition -- 1x1 when shown, which Hyprland would otherwise animate as a slide in from
-- for normal open/close, and skip it for panel handoff. -- the corner. Panels keep their own QML opacity transition for normal
-- open/close, and skip it for panel handoff.
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" }) hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel|omarchy-osd|omarchy-reminders|omarchy-network-qr)$" }, no_anim = true, animation = "none" })
-- Dev gallery is the main shell workbench; open it maximized like -- Dev gallery is the main shell workbench; open it maximized like
+1 -2
View File
@@ -185,8 +185,8 @@
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"}, "setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
"setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"}, "setup.security.sshd": {"icon":"󰣀","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
"setup.security.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"}, "setup.security.ssh-agent": {"icon":"󰣀","label":"SSH Agent","when":"! systemctl --user -q is-enabled gcr-ssh-agent.socket","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-ssh-agent"},
"setup.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
"setup.security.passwordless-sudo": {"icon":"󰟵","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"}, "setup.security.passwordless-sudo": {"icon":"󰟵","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
"setup.security.sudoless-docker": {"icon":"󰡨","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""}, "setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"}, "setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
"setup.config.xcompose": {"icon":"󰞅","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"}, "setup.config.xcompose": {"icon":"󰞅","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
@@ -227,7 +227,6 @@
"install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"}, "install.service.dropbox": {"icon":"","label":"Dropbox","disabled":"omarchy-pkg-present dropbox","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-dropbox"},
"install.service.spotify": {"icon":"󰓇","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"}, "install.service.spotify": {"icon":"󰓇","label":"Spotify","disabled":"omarchy-pkg-present spotify","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-spotify"},
"install.service.signal": {"icon":"󰭹","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"}, "install.service.signal": {"icon":"󰭹","label":"Signal","disabled":"omarchy-pkg-present signal-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-signal"},
"install.service.slack": {"icon":"󰒱","label":"Slack","disabled":"omarchy-pkg-present slack-desktop","action":"omarchy-install-and-launch Slack slack-desktop slack"},
"install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"}, "install.service.tailscale": {"icon":"","label":"Tailscale","disabled":"omarchy-pkg-present tailscale","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-tailscale"},
"install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"}, "install.service.nordvpn": {"icon":"󱇱","label":"NordVPN","disabled":"omarchy-pkg-present nordvpn-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-nordvpn"},
"install.service.once": {"icon":"󰏖","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"}, "install.service.once": {"icon":"󰏖","label":"ONCE","disabled":"omarchy-pkg-present once-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-service-once"},
+5
View File
@@ -0,0 +1,5 @@
[source.crates-io]
replace-with = "aliyun"
[source.aliyun]
registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"
+8
View File
@@ -0,0 +1,8 @@
NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/
GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
RUSTUP_DIST_SERVER=https://mirrors.aliyun.com/rustup
RUSTUP_UPDATE_ROOT=https://mirrors.aliyun.com/rustup/rustup
BUNDLE_MIRROR__ALL=https://mirrors.aliyun.com/rubygems/
MAVEN_ARGS=--global-settings /usr/share/omarchy/default/regions/cn/maven-settings.xml
+3
View File
@@ -0,0 +1,3 @@
---
:sources:
- https://mirrors.aliyun.com/rubygems/
+9
View File
@@ -0,0 +1,9 @@
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<mirrorOf>central</mirrorOf>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>
</settings>
+2
View File
@@ -0,0 +1,2 @@
# Installed from the ISO's offline repository before enabling ArchLinuxCN.
archlinuxcn-keyring
@@ -0,0 +1 @@
Server = https://mirror.omarchy.cn/$repo/os/$arch
@@ -0,0 +1 @@
Server = https://rc-mirror.omarchy.cn/$repo/os/$arch
@@ -0,0 +1 @@
Server = https://stable-mirror.omarchy.cn/$repo/os/$arch
@@ -0,0 +1,2 @@
[archlinuxcn]
Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch
+9
View File
@@ -0,0 +1,9 @@
# Installs in these timezones get this region. The installer's timezone list
# includes tzdata's backward-compatible names, so they are listed too.
Asia/Shanghai
Asia/Urumqi
Asia/Chongqing
Asia/Chungking
Asia/Harbin
Asia/Kashgar
PRC
+6 -10
View File
@@ -1,28 +1,24 @@
# Passwordless sudo # Temporary passwordless sudo
`omarchy-sudo-passwordless` publishes a timed or permanent grant for the numeric UID authenticated by sudo. Its user interface inspects sudo policy without prompting and authenticates only the enable action when access is off, without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`. `omarchy-sudo-passwordless` publishes a bounded grant for the numeric UID authenticated by sudo. Its user interface runs without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
The menu bar's `PasswordlessSudo` indicator polls `--active` every five seconds and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow.
## Grant lifecycle ## Grant lifecycle
The sudoers rule is the only grant record. A timed grant contains the resolved account name and a UTC `NOTAFTER` deadline enforced by sudo itself, including after suspend. Publication validates a dot-prefixed temporary file with `visudo`, arms a calendar cleanup timer, then atomically renames the complete rule into place. There is no separate per-user state file to publish, parse, or reconcile. Failure after renewal starts removes the old grant; failed revocation remains an error and leaves the cleanup timer armed. The sudoers rule is the only grant record: it contains the resolved account name and a UTC `NOTAFTER` deadline enforced by sudo itself, including after suspend. Publication validates a dot-prefixed temporary file with `visudo`, arms a calendar cleanup timer, then atomically renames the complete rule into place. There is no separate per-user state file to publish, parse, or reconcile. Failure after renewal starts removes the old grant; failed revocation remains an error and leaves the cleanup timer armed.
An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface offers a duration picker after result `3`. Result `0` toggles access off when no argument is given, or changes its duration when an argument is given. Permanent access always requires confirmation, including when replacing an active timed grant. It must not turn an inspection failure into a claim that no grant exists. An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface only offers a new grant after result `3`. It must not turn an inspection failure into a claim that no grant exists.
Calendar timers clean up expired files; their liveness does not define authorization. Callbacks read the current rule and remove it only when expired. Earlier callbacks cannot shorten a renewed grant, so no timer identity needs to be persisted. Old UID-only and token-bearing callbacks remain accepted. Pending callbacks after renewal or manual disable are harmless and expire within the maximum 24-hour grant window. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; routine non-boot tmpfiles maintenance leaves live grants alone. Calendar timers clean up expired files; their liveness does not define authorization. Callbacks read the current rule and remove it only when expired. Earlier callbacks cannot shorten a renewed grant, so no timer identity needs to be persisted. Old UID-only and token-bearing callbacks remain accepted. Pending callbacks after renewal or manual disable are harmless and expire within the maximum 24-hour grant window. Boot-time tmpfiles cleanup removes the reserved generated filename namespace before users log in; routine non-boot tmpfiles maintenance leaves live grants alone.
Legacy cleanup uses a root-owned machine marker under `/var/lib/omarchy/migrations/`, written only after successful cleanup under the grant lock. Later accounts can finish their migration queues without sudo and without revoking grants created after the repair. Old grant state files are no longer consulted. A legacy grant is recognized by its exact filename and rule relationship, since the old command wrote the caller's unvalidated name into both, so accounts outside the current name policy are still cleaned up. The generated filename prefix is reserved: boot cleanup and the package hook already remove everything under it, and the old writer could emit a rule whose body differs from its filename, so the migration moves any other file found there into a fresh root-only directory under `/var/lib/omarchy/sudoers-quarantine/`, as `policy` with the original name stored beside it, rather than leaving it live or deleting its content. Legacy cleanup uses a root-owned machine marker under `/var/lib/omarchy/migrations/`, written only after successful cleanup under the grant lock. Later accounts can finish their migration queues without sudo and without revoking grants created after the repair. Old grant state files are no longer consulted. A legacy grant is recognized by its exact filename and rule relationship, since the old command wrote the caller's unvalidated name into both, so accounts outside the current name policy are still cleaned up. The generated filename prefix is reserved: boot cleanup and the package hook already remove everything under it, and the old writer could emit a rule whose body differs from its filename, so the migration moves any other file found there into a fresh root-only directory under `/var/lib/omarchy/sudoers-quarantine/`, as `policy` with the original name stored beside it, rather than leaving it live or deleting its content.
Permanent grants live under `/etc/sudoers.d/99-omarchy-permanent-nopasswd-<uid>`, outside the temporary cleanup namespace. They have no `NOTAFTER` deadline or timer. Status and disable handle both forms; switching duration publishes the replacement before removing the prior policy under the same lock. Old expiry callbacks leave permanent grants intact. If an untrappable interruption leaves both rules, a pair of trusted generated rules for the same account remains inspectable and revocable. Permanent policy continues to govern until an explicit disable or completed duration change; callbacks never remove it merely because the timed companion has expired. Mismatched accounts, symlinks, and administrator-modified companion rules remain errors. Permanent policy survives reboot and package upgrade or removal; disable it before uninstalling the command or downgrading to a version without permanent-grant support, or remove its sudoers file as an administrator afterward.
## Package ownership ## Package ownership
The packaging companion must put the publication/expiry command, `omarchy-security-functions`, `omarchy-nopasswd-sudo.conf`, and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files. The packaging companion must put the publication/expiry command, `omarchy-security-functions`, `omarchy-nopasswd-sudo.conf`, and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing temporary policy. The marker prevents a waiting publisher from creating a new timed grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New timed grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another timed grant. Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing policy. The marker prevents a waiting publisher from creating a new grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another grant.
The runtime marker need not survive reboot: pre-removal revokes the old timed grants before package files disappear, and a new timed grant independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch. The runtime marker need not survive reboot: pre-removal revokes the old grants before package files disappear, and a new invocation independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch.
## Validation ## Validation
+47
View File
@@ -0,0 +1,47 @@
# Regional defaults
Regions customize bootstrap defaults independently of update channels and of the installer's language. Country identifiers are lowercase, officially assigned ISO 3166-1 alpha-2 codes. `global` is a reserved value for no country-specific additions. Only implemented profiles are accepted; currently `global` and `cn`.
## Runtime configuration
The ISO installer writes the region it derived to `/etc/omarchy/region` before target finalization. It is plain text, not executable configuration, owned by root and world-readable (`0644`), because refresh renders as the calling user. A missing file means `global`; an unsupported value is an error.
`omarchy-apply-pacman <stable|rc|edge> [target-root]` restores the selected channel's two pacman templates, appends the region's `pacman.conf.append` to `pacman.conf`, and puts the region's `mirrorlist-<channel>.prepend` ahead of the channel's mirrorlist. Both files are generated before either live file is replaced. Existing files are backed up with `.bak`. Dev uses the edge pacman channel, as before.
Both installation finalization and `omarchy-refresh-pacman` use this command. Explicit refreshes and channel changes restore regional defaults; ordinary updates do not enforce them. The existing `pre-refresh-pacman` user hook runs after regeneration and before the update, so users can maintain overrides there. Switching to the dev channel refuses a checkout that lacks this machine's region profile, because that checkout's own refresh would otherwise drop the regional repositories. Users can edit the files directly or set the region to `global` before a refresh to stop applying regional repository defaults. This does not uninstall packages or rewrite anything in existing home directories.
Refresh renders both files as the calling user and gives root only the finished copies, so the sudo boundary stays command-scoped. That boundary does not defend against other code already running as the same user: such code could change the rendered files while refresh waits for the password, as it could alter that user's own environment in other ways.
The China profile puts Omarchy's China replica of the channel's Arch mirror first (`stable-mirror.omarchy.cn`, `rc-mirror.omarchy.cn`, or `mirror.omarchy.cn` for edge and dev) and keeps the channel's `.org` mirror as the fallback. pacman tries servers in order and moves to the next one when a download fails, including a 404. After three failed downloads from one server, it skips that server for the rest of the transaction. It also takes the package databases from the first server that answers, without comparing them, so the China replicas must carry complete, current metadata. The profile also adds the ArchLinuxCN community repository from USTC's mirror, run by the University of Science and Technology of China. It does not change the `[omarchy]` package repository. ArchLinuxCN is rolling and built against current Arch, while stable and RC are snapshots, so its packages can need newer libraries than a stable machine has.
## Profile layout
Each profile lives in `default/regions/<code>/`:
- `timezones`: the timezones that select this region, one per line, with blank lines and `#` comments allowed. List tzdata's backward-compatible names too, since the installer's timezone list includes them. A timezone may belong to at most one profile.
- `packages`: additional target packages, in the same format. Every ISO carries every profile's packages in its offline repository; the installer installs them only on targets in that region. A package named `<name>-keyring` is the profile's trust anchor: the builder installs it through Arch's existing trust chain, and a regional target populates the `<name>` keyring.
- `pacman/`: `pacman.conf.append`, added after the channel's `pacman.conf`, and one `mirrorlist-<channel>.prepend` per channel (`stable`, `rc`, `edge`), placed ahead of the channel's mirrorlist so the regional mirror is tried first.
## ISO installer contract
There is one ISO for every region. The companion `omarchy-iso` repository reads the profiles from the runtime it bundles, so a region and its repository defaults can never disagree.
- **Build:** every profile's `packages` go into the offline repository, but not into the base package list. The builder adds each profile's community repository to its writable online config to download them; it does not change its Arch channel mirrors.
- **Install:** the installer maps the timezone the user picked to a region through the `timezones` files; a timezone no profile lists means `global`. It shows the result on the summary screen. Unattended installs can set the region explicitly instead. For a regional install it installs the profile's packages from the offline repository and writes `/etc/omarchy/region`; the runtime finalizer then applies the fragments.
- **Deferred installs** have no timezone until first boot, so they are `global` unless the unattended configuration sets a region.
For China, the builder installs `archlinuxcn-keyring` using the existing Arch trust chain before downloading ArchLinuxCN packages. Signature or keyring failures abort the build; no unverified keyserver trust or online signature bypass is added. On a China target the installer installs the keyring from the offline repository, then initializes and populates it before the online repositories are enabled. Global targets never populate ArchLinuxCN's keys. The ISO's existing offline repository verification policy is unchanged.
## Scope of the China profile
The China profile provides repository defaults and developer mirrors. Language is a separate installer choice, not part of a region: a Chinese speaker outside China keeps global mirrors, and a China install in English still gets the regional ones.
## Developer mirrors
The `cn` profile also points the common package managers at mirrors inside China: `environment.conf` (npm, PyPI and uv, Go, rustup, Bundler, and Maven through `MAVEN_ARGS`), `cargo.toml` for crates.io, `gemrc` for RubyGems, and `maven-settings.xml`, which Maven reads in place. During installation `install/config/region.sh` links the first three to `/etc/environment.d/90-omarchy-china.conf`, `/.cargo/config.toml` and `/etc/gemrc` when the marker says `cn`. It refuses to replace a regular file it did not create. The environment reaches sessions started after login.
## Remaining release validation
Unit tests cover region/channel independence, the timezone mapping, generated pacman contents, repeat application, and refresh ordering; refresh itself is exercised only inside the sudo-boundary sandbox. They do not establish live mirror availability, real package trust, or successful offline installation. Before distribution, build from matching local checkouts on an x86_64 builder and run the disposable-VM acceptance workflow in `agents/skills/acceptance-tests.md`.
The acceptance run must install without network access in both directions: a non-China timezone must produce pacman files byte for byte identical to the channel templates, and `Asia/Shanghai` must produce the China profile, boot to a new user, and keep the regional mirrors, repository, marker, and keyring trust through `stable`, `rc`, and `edge` refreshes.
+3 -3
View File
@@ -136,7 +136,7 @@ omarchy-update
├─ omarchy-update-stay-awake start ├─ omarchy-update-stay-awake start
├─ run system-package updates ├─ run system-package updates
├─ run migrations ├─ run migrations
├─ automatically remove orphan packages and run log analysis ├─ run orphan review and log analysis
├─ omarchy-update-status ├─ omarchy-update-status
│ └─ refresh or clear the shell update indicator │ └─ refresh or clear the shell update indicator
├─ restart marked services and the shell ├─ restart marked services and the shell
@@ -158,7 +158,7 @@ Important behavior:
- This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow. - This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow.
- Sleep inhibition authenticates before detaching, drops the held command back to the caller, and closes both update lock descriptors before the persistent process starts. Cleanup accepts only caller-owned, mode-0600, single-link state and revalidates the recorded PID, process start time, owner, and random token immediately before every signal. - Sleep inhibition authenticates before detaching, drops the held command back to the caller, and closes both update lock descriptors before the persistent process starts. Cleanup accepts only caller-owned, mode-0600, single-link state and revalidates the recorded PID, process start time, owner, and random token immediately before every signal.
- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit. - Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit.
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Conflict handoff reports and skips instead of blocking. Orphan packages are automatically removed during every update. Privileged commands still require the one sudo authorization, and AUR installs can prompt separately. - `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Interactive review steps (orphan removal, conflict handoff) report and skip instead of blocking. Privileged commands still require the one sudo authorization, and AUR installs can prompt separately.
- The free-space requirement uses a 10 GiB threshold and stops the update before - The free-space requirement uses a 10 GiB threshold and stops the update before
confirmation when it is not met. If free space cannot be determined, the confirmation when it is not met. If free space cannot be determined, the
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check. check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
@@ -306,7 +306,7 @@ scripts.
| `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. | | `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. |
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. | | `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. | | `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal unless passed `-y`, as the update pipeline does; standalone noninteractive mode only reports. | **Keep for now.** Automates cleanup during updates and supports standalone review. | | `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. | | `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. | | `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. | | `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
+1
View File
@@ -1,3 +1,4 @@
run_logged "$OMARCHY_INSTALL/config/region.sh"
run_logged "$OMARCHY_INSTALL/config/theme-system.sh" run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh" run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
+33
View File
@@ -0,0 +1,33 @@
cn_links=(
"environment.conf /etc/environment.d/90-omarchy-china.conf"
"cargo.toml /.cargo/config.toml"
"gemrc /etc/gemrc"
)
# The installer writes the marker from the timezone.
region=global
if [[ -e /etc/omarchy/region ]]; then
region=$(</etc/omarchy/region)
fi
case $region in
global) ;;
cn)
for link in "${cn_links[@]}"; do
read -r _ target <<<"$link"
if [[ -e $target && ! -L $target ]]; then
echo "Refusing to replace $target, which Omarchy did not create" >&2
exit 1
fi
done
for link in "${cn_links[@]}"; do
read -r source target <<<"$link"
mkdir -p "${target%/*}"
ln -sfn "$OMARCHY_PATH/default/regions/cn/$source" "$target"
done
;;
*)
echo "Unknown region: $region" >&2
exit 1
;;
esac
-1
View File
@@ -18,7 +18,6 @@ run_logged "$OMARCHY_INSTALL/hardware/intel/ipu7-camera.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/fred.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/fix-wifi7-eht.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh" run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh"
run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-ptl-speaker-firmware.sh"
run_logged "$OMARCHY_INSTALL/hardware/fix-elgato-camlink-4k.sh" run_logged "$OMARCHY_INSTALL/hardware/fix-elgato-camlink-4k.sh"
@@ -1,3 +0,0 @@
if omarchy-hw-dell-xps13-dx13260-ptl; then
omarchy-pkg-add linux-firmware-cirrus-dx13260
fi
+1 -1
View File
@@ -21,12 +21,12 @@ cups
cups-filters cups-filters
cups-pk-helper cups-pk-helper
ddcutil ddcutil
disktree-bin
docker docker
docker-buildx docker-buildx
docker-compose docker-compose
dosfstools dosfstools
dotnet-runtime dotnet-runtime
dua-cli
evince evince
exfatprogs exfatprogs
expac expac
-1
View File
@@ -59,7 +59,6 @@ linux-firmware-marvell
# Dell laptop support packages # Dell laptop support packages
dell-xps-touchpad-haptics dell-xps-touchpad-haptics
dell-xps13-sidecar-amps dell-xps13-sidecar-amps
linux-firmware-cirrus-dx13260
# Speaker tunings (LV2 limiter every tuning ends in) # Speaker tunings (LV2 limiter every tuning ends in)
lsp-plugins-lv2 lsp-plugins-lv2
+1 -2
View File
@@ -1,7 +1,6 @@
# Configure pacman after package installation completes. Offline target package # Configure pacman after package installation completes. Offline target package
# installs use the live ISO's offline pacman.conf until this final restore. # installs use the live ISO's offline pacman.conf until this final restore.
cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf "$OMARCHY_PATH/bin/omarchy-apply-pacman" "${OMARCHY_MIRROR:-stable}"
cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist
# Wait for CUPS to own the file, the way omarchy-settings does, so pacman does # Wait for CUPS to own the file, the way omarchy-settings does, so pacman does
# not turn the override into a .pacnew during ISO package installation. # not turn the override into a .pacnew during ISO package installation.
+2 -2
View File
@@ -38,7 +38,7 @@ There are terminal shortcuts too: `a` runs the default agent inline in the curre
### The agents panel ### The agents panel
The top bar has an agents icon. Before you've set up an agent, its panel walks you through signing in to Claude, Codex, or Grok, and the first one becomes your default agent. After that, the panel puts every subscription on one page: each agent and account with its plan, a meter for each of its 5-hour session and weekly limits with the time until it resets (hover for the exact percentage), or the remaining prepaid balance, under a line that rotates through what you've used across all of them, like tokens this week and your most used model. Under _Make something_ it offers starter prompts for a new theme, plugin, or app that hand the task to your default agent. Apps are built the way Omarchy's own Hype, Monologue, and Omacut are, in C++ and Qt Quick, following the omarchy-app agent skill, and the compiler and Qt pieces they need come installed. Claude Code, Codex, Grok, and Fireworks are covered out of the box. The top bar has an agents icon. Before you've set up an agent, its panel walks you through signing in to Claude, Codex, or Grok, and the first one becomes your default agent. After that, the panel puts every subscription on one page: each agent and account with its plan and the percentage used of its 5-hour session and weekly limits (or the remaining prepaid balance), under a line that rotates through what you've used across all of them, like tokens this week and your most used model. Under _Make something_ it offers starter prompts for a new theme, plugin, or app that hand the task to your default agent. Apps are built the way Omarchy's own Hype, Monologue, and Omacut are, in C++ and Qt Quick, following the omarchy-app agent skill, and the compiler and Qt pieces they need come installed. Claude Code, Codex, Grok, and Fireworks are covered out of the box.
Left-click the bar icon for the panel, right-click to launch your default agent. The usage records behind it are regenerated every 15 minutes by `omarchy agent usage-update`, and the panel can even merge usage from your other machines via a synced folder. See the README under `$OMARCHY_PATH/shell/plugins/agents/` for the full settings. Left-click the bar icon for the panel, right-click to launch your default agent. The usage records behind it are regenerated every 15 minutes by `omarchy agent usage-update`, and the panel can even merge usage from your other machines via a synced folder. See the README under `$OMARCHY_PATH/shell/plugins/agents/` for the full settings.
@@ -50,7 +50,7 @@ One account per provider is active, and every new session starts as it: `cx`, `c
With more than one account, the panel shows each account's limits side by side, with the active one marked. By default Omarchy notifies you when the active account passes 95% of a limit, and clicking the notification switches to the account with the most headroom. Hover an account's line in the panel and click the Autoswitch that appears beside Use (or press `m`), or run `omarchy agent account mode auto`, to have it switch on its own instead; while it's on, Autoswitch takes Use's place, and clicking it again goes back to notifying. Add a number to change the threshold, as in `omarchy agent account mode auto 90`. If every account is over the threshold, you'll hear about it once, along with which account frees up first. While an active account is within 15 points of its threshold (80% at the default), its limits are checked every three minutes rather than every 15. With more than one account, the panel shows each account's limits side by side, with the active one marked. By default Omarchy notifies you when the active account passes 95% of a limit, and clicking the notification switches to the account with the most headroom. Hover an account's line in the panel and click the Autoswitch that appears beside Use (or press `m`), or run `omarchy agent account mode auto`, to have it switch on its own instead; while it's on, Autoswitch takes Use's place, and clicking it again goes back to notifying. Add a number to change the threshold, as in `omarchy agent account mode auto 90`. If every account is over the threshold, you'll hear about it once, along with which account frees up first. While an active account is within 15 points of its threshold (80% at the default), its limits are checked every three minutes rather than every 15.
`omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours can show _Sign-in required_; click it to sign that account in again right in the panel, or start a session in it, which refreshes the sign-in on its own. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and tools configured with an absolute path to a CLI binary do not follow the switch. An explicit `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` takes precedence over the selected account. `omarchy agent account list` shows every account and its latest limits, `omarchy agent account remove work` forgets one, and `omarchy agent account rename primary Hey` renames one (or click an account's name in the panel). These commands work on your default agent's accounts; to reach the other provider, name it first, as in `omarchy agent account use codex side`. The login you already had in `~/.claude` or `~/.codex` is the _Main_ account and stays where it is. An account left unused for more than about eight hours shows its last known limits until a session runs in it again. MCP servers you had when an account was added come along to it, but ones you add later with `claude mcp add` only reach the account you added them in. Claude Desktop and tools configured with an absolute path to a CLI binary do not follow the switch. An explicit `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, or `GROK_HOME` takes precedence over the selected account.
### Crash diagnosis ### Crash diagnosis
+4
View File
@@ -26,6 +26,10 @@ You start it (or reattach to your existing session) with `Super + Ctrl + Return`
Omarchy has packaged this as _About_ in the Omarchy menu (`Super + Space`). Omarchy has packaged this as _About_ in the Omarchy menu (`Super + Space`).
## Disk Usage
When the drive fills up and you have no idea what's eating it, launch _Disk Usage_ from the app launcher (`Super + Space`). It's [dua](https://github.com/Byron/dua-cli) in interactive mode pointed at the whole file system, so you can walk down into whatever directory is the culprit, sorted biggest first, and delete from right inside it.
## Cliamp ## Cliamp
[Cliamp](https://www.cliamp.stream/) is a retro terminal music player inspired by Winamp 2.x, complete with built-in radio stations for lo-fi beats. Launch it with `Super + Shift + Alt + M`, or from the Omarchy menu under _Apps_. Press `?` for the full keybinding list. [Cliamp](https://www.cliamp.stream/) is a retro terminal music player inspired by Winamp 2.x, complete with built-in radio stations for lo-fi beats. Launch it with `Super + Shift + Alt + M`, or from the Omarchy menu under _Apps_. Press `?` for the full keybinding list.
-4
View File
@@ -8,10 +8,6 @@ Plug in a USB stick or an SD card and it's mounted automatically, so it just sho
Double-clicking follows sensible defaults: images open in imv, video in mpv, PDFs in Document Viewer, and plain text in Neovim. Double-clicking follows sensible defaults: images open in imv, video in mpv, PDFs in Document Viewer, and plain text in Neovim.
## Disktree
When the drive fills up and you have no idea what's eating it, launch [Disktree](https://github.com/tobi/disktree) from the app launcher (`Super + Space`). It shows a graphical treemap of your home directory. Larger blocks take up more space; walk into directories with the keyboard or mouse, mark what should go, and review your selections before removing anything. Run `disktree --disk` to scan the whole disk.
## Obsidian ## Obsidian
[Obsidian](https://obsidian.md/) is a free and highly extensible note taking application that uses simple Markdown files for storage. [Obsidian](https://obsidian.md/) is a free and highly extensible note taking application that uses simple Markdown files for storage.
-4
View File
@@ -18,10 +18,6 @@ You start 1Password with `Super + Shift + /`. If it isn't installed yet, that ho
You start Spotify using `Super + Shift + M`. Like 1Password, the hotkey kicks off the installation first if Spotify isn't installed yet (or use _Install > Service > Spotify_ from the Omarchy menu). You start Spotify using `Super + Shift + M`. Like 1Password, the hotkey kicks off the installation first if Spotify isn't installed yet (or use _Install > Service > Spotify_ from the Omarchy menu).
## Slack
[Slack](https://slack.com/) is where a lot of teams do their chatting, both inside the company and with outside collaborators. Install it with _Install > Service > Slack_ from the Omarchy menu, and it'll open as soon as it's ready.
## Dropbox ## Dropbox
[Dropbox](https://www.dropbox.com/) is a great way to sync files between machines while keeping a backup in the cloud. To set it up, select _Install > Service > Dropbox_ from the Omarchy menu. Once it's running, hover the tray in the top right of the bar and right-click the Dropbox icon to finish the setup. [Dropbox](https://www.dropbox.com/) is a great way to sync files between machines while keeping a backup in the cloud. To set it up, select _Install > Service > Dropbox_ from the Omarchy menu. Once it's running, hover the tray in the top right of the bar and right-click the Dropbox icon to finish the setup.
+1 -3
View File
@@ -20,9 +20,7 @@ It works by restoring the baseline snapshot the installer takes, so it's only av
## Passwordless sudo ## Passwordless sudo
Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ asks how long to allow access: **15 minutes**, **1 Hour**, **1 Day**, or **Permanently**. A red warning icon appears beside the other menu bar indicators while access is active. Click it or run the command again to turn access off. You can also pass your own number of minutes (from 1 to 1440) with `omarchy-sudo-passwordless 30`, or use `omarchy-sudo-passwordless permanent`. Sometimes you want `sudo` to stop asking, most often when an AI agent is doing a long stretch of system work for you. _Setup > Security > Passwordless Sudo_ turns that off for 15 wall-clock minutes and then puts it back automatically, including immediately after resuming from a suspend that crossed the deadline. A package-owned boot-time cleanup rule removes the grant before logins if the computer restarts first. Run the command again before the timer runs out to end it early, and pass your own number of minutes (from 1 to 1440) with `omarchy-sudo-passwordless 30` if 15 isn't enough.
Timed access expires automatically, including immediately after resuming from a suspend that crossed the deadline. Restarting the computer ends it early. Permanent access survives reboots and stays enabled until you disable it.
Updating or removing Omarchy's settings package ends any temporary grant before its expiry support changes. If the command reports an authorization or cleanup error, resolve it before trying to enable another grant; an error does not mean passwordless access is inactive. Updating or removing Omarchy's settings package ends any temporary grant before its expiry support changes. If the command reports an authorization or cleanup error, resolve it before trying to enable another grant; an error does not mean passwordless access is inactive.
+1 -1
View File
@@ -17,7 +17,7 @@ fi
drop_npm_grok() { drop_npm_grok() {
omarchy-cmd-present mise || return 0 omarchy-cmd-present mise || return 0
if [[ -n $(mise ls -g "$npm_grok") ]]; then if [[ -n $(mise ls -g "$npm_grok") ]]; then
mise unuse -y -g "$npm_grok" mise unuse -g "$npm_grok"
fi fi
if [[ -n $(mise ls -i "$npm_grok") ]]; then if [[ -n $(mise ls -i "$npm_grok") ]]; then
mise uninstall -y --all "$npm_grok" mise uninstall -y --all "$npm_grok"
-6
View File
@@ -1,6 +0,0 @@
echo "Install the Dell XPS 13 Panther Lake speaker firmware"
if omarchy-hw-dell-xps13-dx13260-ptl; then
source "$OMARCHY_PATH/install/hardware/dell-xps13-ptl-speaker-firmware.sh"
omarchy-state set reboot-required
fi
-9
View File
@@ -1,9 +0,0 @@
echo "Replace the Disk Usage TUI with Disktree"
omarchy-pkg-add disktree-bin
omarchy-pkg-drop dua-cli
rm -f "$HOME/.local/share/applications/Disk Usage.desktop"
if [[ -d $HOME/.local/share/applications ]]; then
update-desktop-database "$HOME/.local/share/applications"
fi
-15
View File
@@ -1,15 +0,0 @@
echo "Enable overlay scrollbars in Chromium-based browsers"
for flags_file in "$HOME"/.config/{chromium,chrome,microsoft-edge-stable,brave,brave-origin}-flags.conf; do
[[ -f $flags_file ]] || continue
grep -Eq -- '^--enable-features=([^,]+,)*OverlayScrollbar([,:]|$)' "$flags_file" && continue
if grep -q -- '^--enable-features=' "$flags_file"; then
sed -i --follow-symlinks \
-e '/^--enable-features=/ s/$/,OverlayScrollbar/' \
-e '/^--enable-features=/ s/=,/=/' "$flags_file"
else
[[ -n $(tail -c1 "$flags_file") ]] && echo >>"$flags_file"
echo '--enable-features=OverlayScrollbar' >>"$flags_file"
fi
done
+24 -57
View File
@@ -3,9 +3,12 @@ import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
// Map a fresh fullscreen surface on each open. Keep its first frames // A fullscreen overlay whose surface outlives each open. A fresh surface draws
// transparent until Qt receives the output's scale, so it opens sharp without // its first frames before the compositor sends its fractional scale, so an
// retaining a parked surface that can be closed when a monitor disconnects. // overlay mapped per open flashed blurry until the scale arrived. Closed, the
// surface parks as a 1x1, input-less, content-less layer below windows: small
// enough to cost nothing, off the overlay layer so it never blocks direct
// scanout for fullscreen apps. Opening only resizes and raises it.
PanelWindow { PanelWindow {
id: window id: window
@@ -14,31 +17,11 @@ PanelWindow {
property int shownLayer: WlrLayer.Overlay property int shownLayer: WlrLayer.Overlay
property int shownKeyboardFocus: WlrKeyboardFocus.Exclusive property int shownKeyboardFocus: WlrKeyboardFocus.Exclusive
// The surface no longer lands on the focused output by being mapped there,
// so it follows the focused monitor each time it is shown. Unset until the
// first show lets the compositor choose.
property var targetScreen: null property var targetScreen: null
readonly property var targetMonitor: { property Region emptyRegion: Region {}
var monitors = Hyprland.monitors.values
for (var i = 0; i < monitors.length; i++) {
if (targetScreen && monitors[i].name === targetScreen.name) return monitors[i]
}
return null
}
// Wait for fullscreen geometry as well as scale so no smaller opening frame
// is stretched across the output. Fractional scale uses units of 1/120;
// compare in those units to tolerate floating point rounding.
readonly property bool contentReady: shown && backingWindowVisible && !!targetScreen
&& width === targetScreen.width && height === targetScreen.height
&& !!targetMonitor && Math.round(devicePixelRatio * 120) === Math.round(targetMonitor.scale * 120)
property bool contentRevealed: false
onContentReadyChanged: {
contentRevealed = false
// Resize and scale notifications arrive while Qt is still updating the
// window. Reveal on the next turn, once its content layout has caught up.
if (contentReady) Qt.callLater(function() {
if (window.contentReady) window.contentRevealed = true
})
}
function focusedScreen() { function focusedScreen() {
var monitor = Hyprland.focusedMonitor var monitor = Hyprland.focusedMonitor
@@ -46,44 +29,28 @@ PanelWindow {
for (var i = 0; i < Quickshell.screens.length; i++) { for (var i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === name) return Quickshell.screens[i] if (Quickshell.screens[i].name === name) return Quickshell.screens[i]
} }
return Quickshell.screens.length > 0 ? Quickshell.screens[0] : null return null
} }
// Live monitor scaling does not emit a Hyprland monitor event, so its cached onShownChanged: if (shown) targetScreen = focusedScreen() || targetScreen
// scale can lag behind Qt. Refresh on opening and on scale changes while open.
onShownChanged: {
targetScreen = shown ? focusedScreen() : null
if (shown) Hyprland.refreshMonitors()
}
onDevicePixelRatioChanged: if (shown) Hyprland.refreshMonitors()
Connections { visible: true
target: Quickshell
function onScreensChanged() {
if (window.shown && Quickshell.screens.indexOf(window.targetScreen) < 0) {
window.targetScreen = null
// Let the removed output finish closing its old window before mapping
// a replacement, rather than recreating it during Qt's teardown.
Qt.callLater(function() {
if (window.shown && !window.targetScreen) window.targetScreen = window.focusedScreen()
})
}
}
}
visible: shown && !!targetScreen && !!targetMonitor
screen: targetScreen screen: targetScreen
anchors { top: true; left: true; bottom: true; right: true } anchors { top: true; left: true; bottom: shown; right: shown }
implicitWidth: 1
implicitHeight: 1
mask: shown ? null : emptyRegion
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: shownLayer WlrLayershell.layer: shown ? shownLayer : WlrLayer.Bottom
WlrLayershell.keyboardFocus: shownKeyboardFocus WlrLayershell.keyboardFocus: shown ? shownKeyboardFocus : WlrKeyboardFocus.None
// Opacity preserves keyboard handling while the scale arrives, including // Draw nothing until the surface has actually grown. A frame drawn while it
// search keystrokes and Escape pressed immediately after opening. // is still 1x1 holds only the scrim's color, which the compositor would
// stretch across the whole screen until the fullscreen frame arrives.
Binding { Binding {
target: window.contentItem target: window.contentItem
property: "opacity" property: "visible"
value: window.contentRevealed ? 1 : 0 value: window.shown && window.width > 1 && window.height > 1
} }
} }
+2 -3
View File
@@ -19,9 +19,8 @@ Every subscription on one page, limits first.
per limit window: its meter and the time until it resets (the exact percentage per limit window: its meter and the time until it resets (the exact percentage
on hover). A model-scoped allowance on the same clock (Claude's Fable weekly on hover). A model-scoped allowance on the same clock (Claude's Fable weekly
limit) is a tick on that window's meter rather than a line of its own; the limit) is a tick on that window's meter rather than a line of its own; the
row's tooltip names it. A lapsed or missing sign-in shows a _Sign-in required_ link that signs row's tooltip names it. Sign-in and endpoint trouble shows under the name in the urgent
that account in again from the panel; other endpoint trouble shows under the color. Limits kept from an earlier check after a failed one dim, and their
name in the urgent color. Limits kept from an earlier check after a failed one dim, and their
tooltip says how old they are. tooltip says how old they are.
- **Accounts** — an agent with more than one subscription account (see - **Accounts** — an agent with more than one subscription account (see
`omarchy agent account`) lists each: name and plan on one line (the email on hover), and its own limit `omarchy agent account`) lists each: name and plan on one line (the email on hover), and its own limit
@@ -1,71 +0,0 @@
import QtQuick
import Quickshell.Io
import qs.Commons
import qs.Ui
BarIndicator {
id: root
property bool granted: false
property bool refreshPending: false
active: granted
activeText: "󰟵"
inactiveText: "󰟵"
activeTooltipText: "Disable Passwordless Sudo"
inactiveTooltipText: "Passwordless Sudo"
useActiveColor: true
activeColor: Color.urgent
function refresh() {
if (!root.bar) return
if (statusProc.running) {
root.refreshPending = true
return
}
root.refreshPending = false
statusProc.running = true
}
onBarChanged: refresh()
Component.onCompleted: refresh()
Connections {
target: root.indicatorHost
ignoreUnknownSignals: true
function onRefreshRequested() { root.refresh() }
}
Timer {
interval: 5000
repeat: true
running: !!root.bar
onTriggered: root.refresh()
}
Process {
id: statusProc
command: ["omarchy-sudo-passwordless", "--active"]
onExited: function(exitCode, exitStatus) {
root.granted = exitCode === 0 && exitStatus === 0
if (root.refreshPending) Qt.callLater(root.refresh)
}
}
Process {
id: disableProc
command: ["omarchy-sudo-passwordless", "--disable"]
onExited: function(exitCode, exitStatus) {
if ((exitCode !== 0 || exitStatus !== 0) && root.bar)
root.bar.run('omarchy-notification-send "Could not disable passwordless sudo" "Check the sudo configuration and try again."')
if (root.indicatorHost) root.indicatorHost.refresh()
else root.refresh()
}
}
onPressed: function() {
if (!root.bar || disableProc.running) return
if (root.granted) disableProc.running = true
else root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless")
}
}
@@ -26,11 +26,6 @@
"placeholderText": "Search indicators...", "placeholderText": "Search indicators...",
"emptyText": "No indicators", "emptyText": "No indicators",
"options": [ "options": [
{
"value": "PasswordlessSudo",
"label": "Passwordless sudo",
"description": "Warning while passwordless root access is enabled"
},
{ {
"value": "Dictation", "value": "Dictation",
"label": "Dictation", "label": "Dictation",
+1 -1
View File
@@ -8,7 +8,7 @@ BarWidget {
id: root id: root
moduleName: "omarchy.indicators" moduleName: "omarchy.indicators"
readonly property var defaultIndicatorEntries: [ "PasswordlessSudo", "Dictation", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ] readonly property var defaultIndicatorEntries: [ "Dictation", "ScreenRecording", "Reminder", "NightLight", "Dnd", "StayAwake" ]
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings) readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: [] property var activeIndicatorIds: []
property var indicatorActiveStates: ({}) property var indicatorActiveStates: ({})
+2 -2
View File
@@ -1075,7 +1075,7 @@ Item {
// then on the card grows and shrinks downward instead of re-centering // then on the card grows and shrinks downward instead of re-centering
// on every resize, which made the menu jump around. The rows height is // on every resize, which made the menu jump around. The rows height is
// frozen at the same moment, so the starting menu also caps how tall the // frozen at the same moment, so the starting menu also caps how tall the
// card may grow from there. Closing or changing screens unfreezes both. // card may grow from there. Closing unfreezes both.
property int cardTop: -1 property int cardTop: -1
property int maxRowsHeight: -1 property int maxRowsHeight: -1
readonly property int centeredTop: Math.max(Style.gapsOut, Math.round((height - root.cardHeight) / 2)) readonly property int centeredTop: Math.max(Style.gapsOut, Math.round((height - root.cardHeight) / 2))
@@ -1086,8 +1086,8 @@ Item {
maxRowsHeight = root.visibleRowsHeight maxRowsHeight = root.visibleRowsHeight
} }
} }
// The surface stays mapped between opens, so closing is shown going false.
onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 } onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }
onTargetScreenChanged: { cardTop = -1; maxRowsHeight = -1 }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+7 -1
View File
@@ -81,11 +81,17 @@ layer_absent() {
! layer_present "$1" ! layer_present "$1"
} }
# Fullscreen overlays map on the overlay layer and unmap when closed. # Overlays built on OverlayWindow (the menu, image selector, emoji picker,
# clipboard and the like) stay mapped between opens, parked 1x1 on the bottom
# layer, so they keep their fractional scale. Showing means on the overlay.
layer_on_overlay() { layer_on_overlay() {
hyprctl -j layers | jq -e --arg ns "$1" '[.[].levels["3"][]? | select(.namespace == $ns)] | length > 0' hyprctl -j layers | jq -e --arg ns "$1" '[.[].levels["3"][]? | select(.namespace == $ns)] | length > 0'
} }
layer_off_overlay() {
! layer_on_overlay "$1"
}
# A layer can be mapped but parked off the monitor: the bar hides that way so # A layer can be mapped but parked off the monitor: the bar hides that way so
# revealing it does not have to rebuild the surface. Assert on geometry when # revealing it does not have to rebuild the surface. Assert on geometry when
# what matters is that the user can actually see it. Layer boxes are local to # what matters is that the user can actually see it. Layer boxes are local to
+1 -1
View File
@@ -86,7 +86,7 @@ screenshot "success-menu-05-position-submenu"
wtype -k Down -k Down -k Return wtype -k Down -k Down -k Return
wait_until "menu bar position changes to left" 20 bar_position_is "left" wait_until "menu bar position changes to left" 20 bar_position_is "left"
wait_until "menu bar becomes vertical" 20 bar_is_vertical wait_until "menu bar becomes vertical" 20 bar_is_vertical
wait_until "menu closes after selecting a position" 15 layer_absent "omarchy-menu" wait_until "menu closes after selecting a position" 15 layer_off_overlay "omarchy-menu"
screenshot "success-menu-06-bar-left" screenshot "success-menu-06-bar-left"
if ((config_existed)); then if ((config_existed)); then
-107
View File
@@ -1,107 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
original_monitors=$(hyprctl -j monitors)
remaining_monitor=$(jq -r 'map(select(.focused))[0].name // .[0].name' <<<"$original_monitors")
remaining_height=$(jq -r --arg name "$remaining_monitor" '.[] | select(.name == $name) | (if (.transform // 0) % 2 == 1 then .width else .height end) / .scale | ceil' <<<"$original_monitors")
# Make the source tall enough that a frozen centered menu would land below
# the remaining display, so a mapped but misplaced menu cannot pass recovery.
tall_height=$((remaining_height * 2 + 1600))
virtual_monitor=""
cleanup() {
omarchy-shell shell hide omarchy.menu >/dev/null 2>&1 || true
if [[ -n $virtual_monitor ]]; then
hyprctl output remove "$virtual_monitor" >/dev/null 2>&1 || true
fi
hyprctl dispatch "hl.dsp.focus({ monitor = \"$remaining_monitor\" })" >/dev/null 2>&1 || true
}
trap cleanup EXIT
find_virtual_monitor() {
virtual_monitor=$(hyprctl -j monitors | jq -r --argjson previous "$original_monitors" '[.[].name | select(. as $name | $previous | map(.name) | index($name) == null)][0] // empty')
[[ -n $virtual_monitor ]]
}
monitor_is_tall() {
hyprctl -j monitors | jq -e --arg name "$virtual_monitor" --argjson height "$tall_height" 'any(.[]; .name == $name and .height == $height and .scale == 1)'
}
monitor_is_focused() {
hyprctl -j monitors | jq -e --arg name "$1" 'any(.[]; .name == $name and .focused)'
}
menu_on_monitor() {
hyprctl -j layers | jq -e --arg name "$1" '[.[$name].levels["3"][]? | select(.namespace == "omarchy-menu")] | length == 1'
}
monitor_contains() {
local monitor="$1" text="$2"
local snapshot="$ARTIFACTS/monitor-text-$$.png"
local status=0
if timeout 10 grim -o "$monitor" -s 2 "$snapshot" 2>/dev/null; then
tesseract "$snapshot" stdout --psm 11 2>/dev/null | grep -Fi -- "$text" >/dev/null || status=$?
else
status=1
fi
rm -f "$snapshot"
return "$status"
}
create_source_monitor() {
hyprctl output create headless >/dev/null
wait_until "temporary overlay monitor appears" 15 find_virtual_monitor
hyprctl eval "hl.monitor({ output = \"$virtual_monitor\", mode = \"1920x${tall_height}@60\", position = \"auto\", scale = 1 })" >/dev/null
wait_until "temporary overlay monitor has tall geometry" 15 monitor_is_tall
hyprctl dispatch "hl.dsp.focus({ monitor = \"$virtual_monitor\" })" >/dev/null
wait_until "temporary overlay monitor is focused" 15 monitor_is_focused "$virtual_monitor"
}
remove_source_monitor() {
hyprctl output remove "$virtual_monitor" >/dev/null
virtual_monitor=""
hyprctl dispatch "hl.dsp.focus({ monitor = \"$remaining_monitor\" })" >/dev/null
wait_until "remaining monitor is focused" 15 monitor_is_focused "$remaining_monitor"
}
omarchy-shell shell hide omarchy.menu >/dev/null
wait_until "menu starts closed" 15 layer_absent "omarchy-menu"
# A closed overlay must drop its old output and reopen on a connected one.
create_source_monitor
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
wait_until "menu opens on temporary monitor" 15 menu_on_monitor "$virtual_monitor"
wait_until "menu renders on temporary monitor" 15 monitor_contains "$virtual_monitor" "Apps"
screenshot "success-overlay-monitor-01-closed-source"
omarchy-shell shell hide omarchy.menu >/dev/null
wait_until "menu unmaps before monitor removal" 15 layer_absent "omarchy-menu"
remove_source_monitor
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
wait_until "closed menu reopens on remaining monitor" 15 menu_on_monitor "$remaining_monitor"
wait_until "reopened menu renders on remaining monitor" 15 monitor_contains "$remaining_monitor" "Apps"
screenshot "success-overlay-monitor-02-closed-recovered"
omarchy-shell shell hide omarchy.menu >/dev/null
wait_until "reopened menu closes" 15 layer_absent "omarchy-menu"
# Typing freezes the menu's position. Recovery onto a shorter screen must
# unfreeze that position while preserving the search and keyboard handling.
create_source_monitor
omarchy-shell shell summon omarchy.menu '{"menu":"root"}' >/dev/null
wait_until "open recovery menu maps on tall monitor" 15 menu_on_monitor "$virtual_monitor"
wait_until "open recovery menu renders" 15 monitor_contains "$virtual_monitor" "Apps"
screenshot "success-overlay-monitor-03-open-root"
wtype "keybindings"
wait_until "menu search renders on tall monitor" 15 monitor_contains "$virtual_monitor" "Keybindings"
screenshot "success-overlay-monitor-04-open-search"
remove_source_monitor
wait_until "open menu moves to remaining monitor" 15 menu_on_monitor "$remaining_monitor"
wait_until "open menu search renders on shorter monitor" 15 monitor_contains "$remaining_monitor" "Keybindings"
screenshot "success-overlay-monitor-05-open-recovered"
wtype -k Escape
wait_until "recovered menu clears search through keyboard" 15 monitor_contains "$remaining_monitor" "Apps"
wtype -k Escape
wait_until "recovered menu closes through keyboard" 15 layer_absent "omarchy-menu"
+7 -7
View File
@@ -29,7 +29,7 @@ wtype "rocket"
sleep 1 sleep 1
screenshot "success-emoji-picker-search" screenshot "success-emoji-picker-search"
wtype -k Return wtype -k Return
wait_until "emoji picker selection closes" 15 layer_absent "omarchy-emojis" wait_until "emoji picker selection closes" 15 layer_off_overlay "omarchy-emojis"
# Seed two clipboard entries, search for the older one, and copy it back out. # Seed two clipboard entries, search for the older one, and copy it back out.
clipboard_token="Omarchy acceptance clipboard $(date +%s)" clipboard_token="Omarchy acceptance clipboard $(date +%s)"
@@ -44,7 +44,7 @@ wtype "$clipboard_token"
wait_until "clipboard search finds test text" 15 screen_contains "Omarchy acceptance clipboard" wait_until "clipboard search finds test text" 15 screen_contains "Omarchy acceptance clipboard"
screenshot "success-clipboard-search" screenshot "success-clipboard-search"
wtype -M shift -k Return -m shift wtype -M shift -k Return -m shift
wait_until "clipboard selection closes" 15 layer_absent "omarchy-clipboard" wait_until "clipboard selection closes" 15 layer_off_overlay "omarchy-clipboard"
wait_until "clipboard selection restores test text" 15 bash -c '[[ $(wl-paste --no-newline) == "$1" ]]' _ "$clipboard_token" wait_until "clipboard selection restores test text" 15 bash -c '[[ $(wl-paste --no-newline) == "$1" ]]' _ "$clipboard_token"
# Exercise the system branch without invoking any destructive action. # Exercise the system branch without invoking any destructive action.
@@ -53,7 +53,7 @@ wait_until "system menu opens" 15 layer_on_overlay "omarchy-menu"
wait_until "system menu content is visible" 15 screen_contains "Shutdown" wait_until "system menu content is visible" 15 screen_contains "Shutdown"
screenshot "success-system-menu" screenshot "success-system-menu"
wtype -k Escape wtype -k Escape
wait_until "system menu closes" 15 layer_absent "omarchy-menu" wait_until "system menu closes" 15 layer_off_overlay "omarchy-menu"
# Preview both visual selectors and cancel without changing user state. These # Preview both visual selectors and cancel without changing user state. These
# cover thumbnail generation, the image-grid overlay, and current selection. # cover thumbnail generation, the image-grid overlay, and current selection.
@@ -62,14 +62,14 @@ wait_until "background selector opens" 30 layer_on_overlay "omarchy-image-select
sleep 1 sleep 1
screenshot "success-background-selector" screenshot "success-background-selector"
wtype -k Escape wtype -k Escape
wait_until "background selector closes" 15 layer_absent "omarchy-image-selector" wait_until "background selector closes" 15 layer_off_overlay "omarchy-image-selector"
launch_app "omarchy-theme-switcher" launch_app "omarchy-theme-switcher"
wait_until "theme selector opens" 30 layer_on_overlay "omarchy-image-selector" wait_until "theme selector opens" 30 layer_on_overlay "omarchy-image-selector"
sleep 1 sleep 1
screenshot "success-theme-selector" screenshot "success-theme-selector"
wtype -k Escape wtype -k Escape
wait_until "theme selector closes" 15 layer_absent "omarchy-image-selector" wait_until "theme selector closes" 15 layer_off_overlay "omarchy-image-selector"
# Walk the reminder flow through each input screen, but dismiss before it # Walk the reminder flow through each input screen, but dismiss before it
# schedules a real timer in the test user's session. # schedules a real timer in the test user's session.
@@ -83,7 +83,7 @@ wtype -k Return
wait_until "reminder message prompt opens" 15 screen_contains "Reminder message" wait_until "reminder message prompt opens" 15 screen_contains "Reminder message"
screenshot "success-reminder-03-message-prompt" screenshot "success-reminder-03-message-prompt"
wtype -k Escape wtype -k Escape
wait_until "reminder flow closes" 15 layer_absent "omarchy-reminders" wait_until "reminder flow closes" 15 layer_off_overlay "omarchy-reminders"
# Render a real shell notification and clear it through the notification IPC. # Render a real shell notification and clear it through the notification IPC.
omarchy-shell notifications dismissAll >/dev/null omarchy-shell notifications dismissAll >/dev/null
@@ -111,7 +111,7 @@ screenshot "success-apps-menu-search"
wtype -k Return wtype -k Return
wait_until "apps menu launches the top search hit" 60 window_present "(?i)omawrite" wait_until "apps menu launches the top search hit" 60 window_present "(?i)omawrite"
wait_until "apps menu closes after launching" 15 layer_absent "omarchy-menu" wait_until "apps menu closes after launching" 15 layer_off_overlay "omarchy-menu"
close_windows "(?i)omawrite" close_windows "(?i)omawrite"
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite" wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-channel-set copy_boundary_file bin/omarchy-channel-set
copy_boundary_file bin/omarchy-refresh-pacman copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
copy_boundary_file bin/omarchy-update copy_boundary_file bin/omarchy-update
export OMARCHY_UPDATE_LOGGED=1 export OMARCHY_UPDATE_LOGGED=1
+53
View File
@@ -113,6 +113,7 @@ run_channel() {
: >"$log_file" : >"$log_file"
OMARCHY_CHANNEL_TEST_LOG="$log_file" \ OMARCHY_CHANNEL_TEST_LOG="$log_file" \
OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \ OMARCHY_PATH="${OMARCHY_TEST_PATH:-$package_root}" \
OMARCHY_REGION_FILE="${OMARCHY_TEST_REGION_FILE:-$test_tmp/no-region}" \
HOME="$test_tmp/home" \ HOME="$test_tmp/home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \ PATH="$stub_bin:$ROOT/bin:$PATH" \
"${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@" "${OMARCHY_TEST_PATH:-$package_root}/bin/omarchy-channel-set" "$@"
@@ -190,6 +191,58 @@ fi
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy" assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy"
pass "switching back to dev reuses the existing ~/omarchy checkout" pass "switching back to dev reuses the existing ~/omarchy checkout"
printf 'cn\n' >"$test_tmp/region"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a checkout without this machine's region"
fi
grep -q "does not support the 'cn' region" "$test_tmp/region.err" || fail "dev explains the missing region" "$(cat "$test_tmp/region.err")"
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses before linking or refreshing pacman" "$(cat "$log_file")"
fi
pass "dev refuses a checkout without the region before touching pacman"
# A directory with the region's name, or a helper that cannot run, is not
# support: the refresh after linking would fail with dev already linked.
mkdir -p "$checkout/default/regions/cn"
touch "$checkout/bin/omarchy-apply-pacman"
chmod +x "$checkout/bin/omarchy-apply-pacman"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses an empty region profile"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses an empty region profile before linking or refreshing pacman" "$(cat "$log_file")"
fi
mkdir -p "$checkout/default/regions/cn/pacman"
cn_fragments=(pacman.conf.append mirrorlist-stable.prepend mirrorlist-rc.prepend mirrorlist-edge.prepend)
for missing in "${cn_fragments[@]}"; do
for fragment in "${cn_fragments[@]}"; do
if [[ $fragment == "$missing" ]]; then
rm -f "$checkout/default/regions/cn/pacman/$fragment"
else
touch "$checkout/default/regions/cn/pacman/$fragment"
fi
done
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a region profile without $missing"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses a profile without $missing before linking or refreshing pacman" "$(cat "$log_file")"
fi
done
touch "${cn_fragments[@]/#/$checkout/default/regions/cn/pacman/}"
chmod -x "$checkout/bin/omarchy-apply-pacman"
if OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev >"$test_tmp/region.out" 2>"$test_tmp/region.err"; then
fail "dev refuses a checkout whose pacman helper cannot run"
fi
if grep -Eq $'^(link|refresh)\t' "$log_file"; then
fail "dev refuses incomplete region support before linking or refreshing pacman" "$(cat "$log_file")"
fi
pass "dev refuses a region profile missing any fragment or a helper that cannot run"
chmod +x "$checkout/bin/omarchy-apply-pacman"
OMARCHY_TEST_REGION_FILE="$test_tmp/region" run_channel dev
assert_log_line $'refresh\tedge' "dev proceeds with a checkout that supports the region"
current_channel() { current_channel() {
OMARCHY_TEST_VERSION_CHANNEL="$1" \ OMARCHY_TEST_VERSION_CHANNEL="$1" \
OMARCHY_TEST_PACKAGES="$2" \ OMARCHY_TEST_PACKAGES="$2" \
@@ -1,46 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1791376887.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
home="$test_tmp/home"
config_dir="$home/.config"
mkdir -p "$config_dir" "$test_tmp/targets"
printf '%s\n' '--enable-features=TouchpadOverscrollHistoryNavigation' >"$config_dir/chromium-flags.conf"
printf '%s\n' '--enable-features=' >"$config_dir/chrome-flags.conf"
printf '%s' '--ozone-platform=wayland' >"$config_dir/microsoft-edge-stable-flags.conf"
printf '%s\n' '--enable-features=BraveFeature' >"$test_tmp/targets/brave-flags.conf"
ln -s "$test_tmp/targets/brave-flags.conf" "$config_dir/brave-flags.conf"
run_migration() {
HOME="$home" bash -euo pipefail "$migration" >/dev/null
}
run_migration
run_migration
grep -Fxq -- '--enable-features=TouchpadOverscrollHistoryNavigation,OverlayScrollbar' "$config_dir/chromium-flags.conf" ||
fail "the migration preserves existing Chromium features"
overlay_count=$(grep -o 'OverlayScrollbar' "$config_dir/chromium-flags.conf" | wc -l)
(( overlay_count == 1 )) || fail "the migration does not duplicate the Chromium feature"
pass "the migration preserves existing features and can run twice"
[[ $(cat "$config_dir/chrome-flags.conf") == '--enable-features=OverlayScrollbar' ]] ||
fail "the migration fills an empty feature list"
[[ $(cat "$config_dir/microsoft-edge-stable-flags.conf") == $'--ozone-platform=wayland\n--enable-features=OverlayScrollbar' ]] ||
fail "the migration adds a feature list with a missing trailing newline"
pass "the migration handles empty and missing feature lists"
[[ -L $config_dir/brave-flags.conf ]] || fail "the migration preserves symlinked browser flags"
grep -Fxq -- '--enable-features=BraveFeature,OverlayScrollbar' "$config_dir/brave-flags.conf" ||
fail "the migration updates the target of symlinked browser flags"
pass "the migration follows symlinked browser flags"
[[ ! -e $config_dir/brave-origin-flags.conf ]] || fail "the migration leaves missing browser flags absent"
pass "the migration skips missing browser flags"
-39
View File
@@ -1,39 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/home/.local/share/applications"
for helper in omarchy-pkg-add omarchy-pkg-drop update-desktop-database; do
cat > "$test_tmp/bin/$helper" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$TEST_LOG"
SH
chmod +x "$test_tmp/bin/$helper"
done
launcher="$test_tmp/home/.local/share/applications/Disk Usage.desktop"
printf '[Desktop Entry]\nExec=dua i /\n' > "$launcher"
for (( attempt = 0; attempt < 2; attempt++ )); do
env HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" TEST_LOG="$test_tmp/log" PATH="$test_tmp/bin:$PATH" \
bash -euo pipefail "$ROOT/migrations/1791345484.sh"
[[ ! -e $launcher ]] || fail "the migration removes the old Disk Usage launcher"
done
pass "the Disktree migration removes the old launcher and can run twice"
expected="omarchy-pkg-add disktree-bin
omarchy-pkg-drop dua-cli
update-desktop-database $test_tmp/home/.local/share/applications"
[[ $(cat "$test_tmp/log") == "$expected"$'\n'"$expected" ]] || fail "the migration installs Disktree before removing dua and refreshing launchers"
pass "the migration installs Disktree before removing dua and refreshing launchers"
rm -r "$test_tmp/home/.local/share/applications"
env HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" TEST_LOG="$test_tmp/log" PATH="$test_tmp/bin:$PATH" \
bash -euo pipefail "$ROOT/migrations/1791345484.sh"
[[ ! -d $test_tmp/home/.local/share/applications ]] || fail "the migration relies on the packaged Disktree launcher"
pass "the migration relies on the packaged Disktree launcher when no local launchers exist"
@@ -52,9 +52,7 @@ case "$name" in
;; ;;
mv) mv)
[[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1 [[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1
[[ ${TEST_REQUIRE_EXISTING_TARGET:-0} != 1 || -f ${@: -1} ]] || exit 1
/usr/bin/mv "$@" /usr/bin/mv "$@"
[[ ${TEST_KILL_AFTER_PUBLISH:-0} != 1 ]] || /usr/bin/kill -KILL "$PPID"
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing" [[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
;; ;;
systemd-run) systemd-run)
@@ -71,15 +69,7 @@ case "$name" in
sudo) sudo)
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
if [[ ${1:-} == -k ]]; then exit 0; fi if [[ ${1:-} == -k ]]; then exit 0; fi
if [[ ${1:-} == -n && ${3:-} == -l ]]; then if [[ ${1:-} == -N ]]; then shift; fi
[[ ${TEST_POLICY_FAILURE:-0} != 1 ]] || exit 1
default_policy=' Options: authenticate'
[[ ${TEST_STATUS:-3} == 3 ]] || default_policy=' Options: !authenticate'
printf '%s\n' "${TEST_POLICY:-$default_policy}"
exit 0
fi
if [[ ${1:-} == -n ]]; then shift; fi
if [[ ${1:-} == -N || ${1:-} == -kn ]]; then shift; fi
if [[ ${1:-} == -- ]]; then shift; fi if [[ ${1:-} == -- ]]; then shift; fi
if [[ ${TEST_MIGRATION:-0} == 1 ]]; then if [[ ${TEST_MIGRATION:-0} == 1 ]]; then
[[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1 [[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1
@@ -88,14 +78,7 @@ case "$name" in
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}" [[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
fi fi
;; ;;
gum) gum) exit 1 ;;
if [[ $1 == choose ]]; then
[[ ${TEST_CHOICE_CANCEL:-0} != 1 ]] || exit 130
printf '%s\n' "${TEST_CHOICE:-15 minutes}"
else
exit "${TEST_CONFIRM_STATUS:-1}"
fi
;;
*) exit 99 ;; *) exit 99 ;;
esac esac
STUB STUB
@@ -137,6 +120,6 @@ assert_status() {
(( actual == expected )) || fail "expected status $expected, got $actual from $*" (( actual == expected )) || fail "expected status $expected, got $actual from $*"
} }
reset_grant() { reset_grant() {
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-1000" "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing" rm -f "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
: >"$test_tmp/commands" : >"$test_tmp/commands"
} }
+9 -1
View File
@@ -9,7 +9,11 @@ export SUDO_TEST_LOG="$boundary_tmp/events"
export SUDO_TEST_CACHE="$boundary_tmp/cache" export SUDO_TEST_CACHE="$boundary_tmp/cache"
export OMARCHY_PATH="$SUDO_TEST_ROOT" export OMARCHY_PATH="$SUDO_TEST_ROOT"
export SUDO_TEST_HOME="$boundary_tmp/home" export SUDO_TEST_HOME="$boundary_tmp/home"
mkdir -p "$SUDO_TEST_HOME" # The machine the render reads its region marker from. It starts global; a test
# writes etc/omarchy/region here to model a regional install, never the host's.
export SUDO_TEST_TARGET="$boundary_tmp/target"
export OMARCHY_REGION_FILE="$SUDO_TEST_TARGET/etc/omarchy/region"
mkdir -p "$SUDO_TEST_HOME" "$SUDO_TEST_TARGET/etc"
mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update" mkdir -p "$SUDO_TEST_ROOT/bin" "$SUDO_TEST_ROOT/mock" "$SUDO_TEST_ROOT/default/omarchy/sudo-no-update"
: >"$SUDO_TEST_LOG" : >"$SUDO_TEST_LOG"
@@ -24,6 +28,10 @@ s=(source/name).read_text().replace('$HOME', '$SUDO_TEST_HOME')
for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']: for command in ['sudo','pkexec','pacman','omarchy-pkg-missing','systemd-inhibit','setpriv','snapper']:
s=s.replace('/usr/bin/'+command, str(target/'mock'/command)) s=s.replace('/usr/bin/'+command, str(target/'mock'/command))
s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"') s=s.replace('PATH=/usr/bin:/usr/sbin:/bin:/sbin', 'PATH="'+str(target/'bin')+':/usr/bin:/usr/sbin:/bin:/sbin"')
if name.name == 'omarchy-apply-pacman':
render_etc='etc="${4:-/}/etc"'
assert s.count(render_etc)==1, 'render target lookup moved'
s=s.replace(render_etc, 'etc="${4:-$SUDO_TEST_TARGET}/etc"')
p.write_text(s) p.write_text(s)
p.chmod((source/name).stat().st_mode & 0o777) p.chmod((source/name).stat().st_mode & 0o777)
PY PY
+1 -1
View File
@@ -147,7 +147,7 @@ assert(
) )
assert( assert(
/OverlayWindow \{\s*id: panel\s*shown: root\.opened\s*shownKeyboardFocus: root\.imagesLoaded \? WlrKeyboardFocus\.Exclusive : WlrKeyboardFocus\.None/.test(imagePickerQml), /OverlayWindow \{\s*id: panel\s*shown: root\.opened\s*shownKeyboardFocus: root\.imagesLoaded \? WlrKeyboardFocus\.Exclusive : WlrKeyboardFocus\.None/.test(imagePickerQml),
'image picker uses OverlayWindow and takes the keyboard once images load' 'image picker parks on OverlayWindow and takes the keyboard once images load'
) )
assert( assert(
/model: visibleImages/.test(imagePickerQml) && /model: visibleImages/.test(imagePickerQml) &&
+1 -212
View File
@@ -32,7 +32,7 @@ for status in 1 2 3; do
else else
(( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement" (( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement"
fi fi
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands" || fail "status must inspect policy without authentication" grep -q '^sudo -N -- .* __status ' "$test_tmp/commands" || fail "status must not publish reusable authorization"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization" [[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization"
done done
pass "public status distinguishes inactive from errors and revokes authorization on exit" pass "public status distinguishes inactive from errors and revokes authorization on exit"
@@ -164,214 +164,3 @@ cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/
[[ ! -e $(rule_file 1000) ]] [[ ! -e $(rule_file 1000) ]]
) )
pass "trailing-dollar accounts publish valid native policy and alias-shaped names never publish" pass "trailing-dollar accounts publish valid native policy and alias-shaped names never publish"
reset_grant
(
source "$library"
valid_duration permanent
! valid_duration forever || fail "invalid duration was accepted"
enable_locked 1000 permanent
read_grant 1000
[[ $GRANT_NAME == audituser && -z $GRANT_DEADLINE ]]
[[ $(stat -c '%a' "$(rule_file 1000 permanent)") == 440 ]]
/usr/sbin/visudo -cf "$(rule_file 1000 permanent)" >/dev/null
! grep -q '^systemd-run ' "$test_tmp/commands" || fail "permanent grant started a timer"
TEST_EXPIRED=1 status_locked 1000
TEST_EXPIRED=1 expire_locked 1000
cleanup_all_locked
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --boot --inline 'r! /etc/sudoers.d/99-omarchy-nopasswd-*'
[[ -f $(rule_file 1000 permanent) ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 1000
)
pass "permanent access has no timer, survives expiry and temporary cleanup, and can be disabled"
reset_grant
(
source "$library"
enable_locked 1000 15
enable_locked 1000 permanent
[[ ! -e $(rule_file 1000) && -e $(rule_file 1000 permanent) ]]
TEST_EXPIRED=1 expire_locked 1000
[[ -e $(rule_file 1000 permanent) ]]
enable_locked 1000 60
[[ -e $(rule_file 1000) && ! -e $(rule_file 1000 permanent) ]]
TEST_EXPIRED=1 expire_locked 1000
assert_status 3 status_locked 1000
TEST_PUBLISH_FAIL=1 assert_status 1 enable_locked 1000 permanent
[[ ! -e $(rule_file 1000 permanent) ]]
)
pass "switching duration replaces the prior policy and old callbacks preserve permanent access"
reset_grant
(
source "$library"
permanent=$(rule_file 1000 permanent)
printf 'audituser ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$permanent"
assert_status 2 status_locked 1000
assert_status 1 enable_locked 1000 permanent
assert_status 1 cleanup_uid_locked 1000
[[ -e $permanent ]]
rm "$permanent"
enable_locked 1000 permanent
TEST_BAD_PATH="$permanent" assert_status 2 status_locked 1000
)
pass "permanent policy rejects unsafe ownership and preserves administrator edits"
for choice in '15 minutes' '1 Hour' '1 Day' Permanently; do
case "$choice" in
'15 minutes') expected=15 ;;
'1 Hour') expected=60 ;;
'1 Day') expected=1440 ;;
Permanently) expected=permanent ;;
esac
: >"$test_tmp/commands"
TEST_CHOICE="$choice" TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
grep -q '^gum choose .*15 minutes 1 Hour 1 Day Permanently$' "$test_tmp/commands"
grep -q "^sudo -N -- .* __enable .* $expected$" "$test_tmp/commands"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
if [[ $expected == permanent ]]; then
grep -q 'remain enabled across reboots until you disable it' "$test_tmp/public.log"
! grep -q 'automatically disable' "$test_tmp/public.log" || fail "permanent grant claims automatic expiry"
fi
done
pass "each duration choice dispatches its exact grant duration with permanent-specific copy"
: >"$test_tmp/commands"
assert_status 130 env TEST_CHOICE_CANCEL=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
! grep -q '__enable\|^gum confirm ' "$test_tmp/commands" || fail "cancelled picker continued enablement"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
: >"$test_tmp/commands"
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
! grep -q '^gum ' "$test_tmp/commands" || fail "disabling access offered a picker"
grep -q '__disable ' "$test_tmp/commands"
pass "cancelling the picker grants nothing and active access still toggles off"
for confirm_status in 0 1; do
: >"$test_tmp/commands"
TEST_STATUS=0 TEST_CONFIRM_STATUS=$confirm_status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" permanent >"$test_tmp/public.log" 2>&1
grep -q '^gum confirm Enable passwordless sudo permanently?' "$test_tmp/commands"
if (( confirm_status == 0 )); then
grep -q '__enable .* permanent$' "$test_tmp/commands"
else
! grep -q '__enable ' "$test_tmp/commands" || fail "declining permanent access still enabled it"
fi
done
pass "changing active access to permanent always requires confirmation"
reset_grant
(
source "$library"
enable_locked 1000 15
TEST_REQUIRE_EXISTING_TARGET=1 enable_locked 1000 60
cleanup_uid_locked 1000
enable_locked 01000 permanent
status_locked 1000
[[ -e $(rule_file 1000 permanent) ]]
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-01000 ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 01000
)
pass "renewal replaces the existing rule atomically and zero-padded UIDs share one policy"
for policy in ' Options: !authenticate' ' Options: authenticate'; do
: >"$test_tmp/commands"
expected=1
[[ $policy != *'!authenticate'* ]] || expected=0
assert_status "$expected" env TEST_POLICY="$policy" /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
[[ $(wc -l <"$test_tmp/commands") == 1 ]]
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands"
done
assert_status 1 env TEST_POLICY_FAILURE=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
pass "menu probe reads policy tags without executing a privileged action or changing the timestamp"
for status in 0 3; do
: >"$test_tmp/commands"
TEST_STATUS=$status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "explicit disable offered enablement"
if (( status == 0 )); then
grep -q '__disable ' "$test_tmp/commands"
fi
done
pass "explicit disable never enables access even when a displayed grant has expired"
: >"$test_tmp/commands"
TEST_STATUS=3 TEST_CHOICE='15 minutes' TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "enable must authenticate exactly one sudo call"
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
! grep -q '^sudo -n -N -- .* __status ' "$test_tmp/commands" || fail "inactive flow attempted root status"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
pass "enabling from inactive policy has exactly one password-capable sudo invocation"
: >"$test_tmp/commands"
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands" || fail "disable must refuse interactive authentication"
! grep -q '^sudo -N -- ' "$test_tmp/commands" || fail "disable attempted an interactive sudo command"
pass "disabling active access is fully noninteractive"
for duration in permanent 15; do
reset_grant
(
source "$library"
if [[ $duration == permanent ]]; then
enable_locked 1000 15
else
enable_locked 1000 permanent
fi
assert_status 137 env TEST_KILL_AFTER_PUBLISH=1 TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __enable 1000 "$duration"
[[ -f $(rule_file 1000) && -f $(rule_file 1000 permanent) ]]
status_locked 1000
TEST_EXPIRED=1 expire_locked 1000
[[ -f $(rule_file 1000 permanent) ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 1000
)
done
pass "SIGKILL during either duration switch leaves access inspectable and revocable without expiring permanent policy"
reset_grant
(
source "$library"
enable_locked 1000 permanent
timed=$(rule_file 1000)
printf 'otheruser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
assert_status 2 read_grant 1000
assert_status 1 enable_locked 1000 15
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
TEST_BAD_PATH="$timed" assert_status 2 read_grant 1000
rm "$timed"
ln -s "$(rule_file 1000 permanent)" "$timed"
assert_status 2 read_grant 1000
rm "$timed"
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
enable_locked 1000 15
[[ -f $timed && ! -e $(rule_file 1000 permanent) ]]
)
pass "paired policy recovery rejects mismatched or unsafe rules and allows a complete duration change"
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands"
! grep -q '^gum\|^sudo -N -- ' "$test_tmp/commands" || fail "listpw=always disable prompted"
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=1 TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "listing failure must leave one enable authentication"
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
: >"$test_tmp/commands"
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "unsafe grant was offered enablement"
pass "password-required listings do not block enabling, disabling, or active detection"
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
grep -q '^sudo -kn -- .* __status ' "$test_tmp/commands" || fail "fallback probe must ignore cached credentials"
(
source "$library"
with_root_lock() { return 1; }
verify_sudo_caller() { return 0; }
assert_status 2 root_dispatch __status 1000
)
pass "status lock failures remain inspection errors and an authenticated unsafe grant keeps its warning"
+23 -120
View File
@@ -9,122 +9,31 @@ const fs = require('fs')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8') const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
const overlay = read('shell/Ui/OverlayWindow.qml') const overlay = read('shell/Ui/OverlayWindow.qml')
// Closing must unmap, not leave a surface on an output that may disappear. // Closed, the surface parks 1x1, input-less and without keyboard below
// windows: off the overlay layer, so it never blocks direct scanout.
assert( assert(
overlay.includes('visible: shown && !!targetScreen && !!targetMonitor') && overlay.includes('visible: true') &&
overlay.includes('anchors { top: true; left: true; bottom: true; right: true }') && overlay.includes('anchors { top: true; left: true; bottom: shown; right: shown }') &&
!overlay.includes('implicitWidth: 1') && !overlay.includes('WlrLayer.Bottom'), overlay.includes('implicitWidth: 1') &&
'closed overlays unmap instead of parking a surface' overlay.includes('implicitHeight: 1') &&
overlay.includes('mask: shown ? null : emptyRegion') &&
overlay.includes('WlrLayershell.layer: shown ? shownLayer : WlrLayer.Bottom') &&
overlay.includes('WlrLayershell.keyboardFocus: shown ? shownKeyboardFocus : WlrKeyboardFocus.None'),
'overlay window parks a 1x1 input-less surface below windows while hidden'
) )
const readyExpression = /readonly property bool contentReady: ([^\n]*(?:\n &&[^\n]*)*)/.exec(overlay)[1] // Content stays hidden until the surface has grown, so no frame drawn at 1x1
const ready = new Function('shown', 'backingWindowVisible', 'width', 'height', 'targetScreen', 'targetMonitor', 'devicePixelRatio', `return ${readyExpression}`) // is stretched across the screen.
const screen = { name: 'eDP-1', width: 1200, height: 750 }
const monitor = { name: 'eDP-1', scale: 1.6 }
for (const scale of [2, 1, 1.6]) {
assertEqual(ready(true, true, 1200, 750, screen, monitor, scale), scale === 1.6,
`opening frames at ${scale}x wait for the monitor's 1.6x scale`)
}
for (const scale of [1, 1.25, 1.5, 2]) {
assert(ready(true, true, 1200, 750, screen, { scale }, scale),
`overlay reveals at the correct ${scale}x scale`)
}
assert(ready(true, true, 1200, 750, screen, { scale: 1.333333 }, 160 / 120),
'scale comparison tolerates compositor floating point rounding')
assert(!ready(true, true, 500, 500, screen, monitor, 1.6),
'initial window geometry never stretches content across the screen')
assert(!ready(true, false, 1200, 750, screen, monitor, 1.6),
'content waits for a mapped backing window')
assert(!ready(false, true, 1200, 750, screen, monitor, 1.6),
'closed content stays transparent even at the correct scale')
assert(!ready(true, true, 1200, 750, null, monitor, 1.6) &&
!ready(true, true, 1200, 750, screen, null, 1.6),
'content waits for a connected screen and its monitor information')
assert( assert(
/target: window\.contentItem\s*property: "opacity"\s*value: window\.contentRevealed \? 1 : 0/.test(overlay), /target: window\.contentItem\s*property: "visible"\s*value: window\.shown && window\.width > 1 && window\.height > 1/.test(overlay),
'transparent opening frames preserve immediate keyboard handling' 'overlay window draws nothing until the surface has grown'
) )
const revealBody = /onContentReadyChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1] assert(
const reveal = new Function('window', 'Qt', `with (window) { ${revealBody} }`) /onShownChanged: if \(shown\) targetScreen = focusedScreen\(\) \|\| targetScreen/.test(overlay) &&
const reveals = [] overlay.includes('screen: targetScreen'),
const revealQt = { callLater(callback) { reveals.push(callback) } } 'overlay window follows the focused monitor each time it is shown'
const content = { contentReady: true, contentRevealed: false } )
reveal(content, revealQt)
assert(!content.contentRevealed, 'scale and resize notifications finish before content reveals')
reveals.shift()()
assert(content.contentRevealed, 'settled content reveals on the next event-loop turn')
content.contentReady = false
reveal(content, revealQt)
assert(!content.contentRevealed, 'losing readiness immediately hides content')
content.contentReady = true
reveal(content, revealQt)
content.contentReady = false
reveal(content, revealQt)
reveals.shift()()
assert(!content.contentRevealed, 'a close or screen loss cancels a pending reveal')
const focusedScreenBody = /function focusedScreen\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
const focusedScreen = new Function('Hyprland', 'Quickshell', focusedScreenBody)
const external = { name: 'DP-1', width: 1920, height: 1080 }
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen, external] }), external,
'overlay selects the focused output')
assertEqual(focusedScreen({ focusedMonitor: { name: 'DP-1' } }, { screens: [screen] }), screen,
'disconnecting the focused output falls back to a connected screen')
assertEqual(focusedScreen({ focusedMonitor: null }, { screens: [] }), null,
'no outputs leaves the overlay unmapped')
const shownHandler = /onShownChanged: \{([\s\S]*?)\n \}/.exec(overlay)[1]
let refreshed = 0
const Hyprland = { refreshMonitors() { refreshed++ } }
const changeShown = new Function('shown', 'focusedScreen', 'Hyprland', `let targetScreen = null; ${shownHandler}; return targetScreen`)
assertEqual(changeShown(false, () => external, Hyprland), null, 'closing drops the previous screen reference')
assertEqual(refreshed, 0, 'closing does not refresh monitor data')
assertEqual(changeShown(true, () => screen, Hyprland), screen, 'reopening selects a current screen')
assertEqual(refreshed, 1, 'opening refreshes monitor scales after a live change while closed')
const scaleHandler = /onDevicePixelRatioChanged: ([^\n]+)/.exec(overlay)[1]
const changeScale = new Function('shown', 'Hyprland', scaleHandler)
changeScale(false, Hyprland)
assertEqual(refreshed, 1, 'scale changes on hidden windows do not refresh monitor data')
const changedScreen = { name: 'eDP-1', width: 960, height: 600 }
assert(!ready(true, true, 960, 600, changedScreen, monitor, 2),
'a stale monitor scale keeps content transparent after a live scale change')
Hyprland.refreshMonitors = () => { refreshed++; monitor.scale = 2 }
changeScale(true, Hyprland)
assertEqual(refreshed, 2, 'a showing window refreshes monitor scales when Qt receives a live change')
assert(ready(true, true, 960, 600, changedScreen, monitor, 2),
'fresh monitor data lets content reveal at the new scale')
const screensChangedBody = /function onScreensChanged\(\) \{([\s\S]*?)\n \}/.exec(overlay)[1]
const screensChanged = new Function('window', 'Quickshell', 'Qt', screensChangedBody)
const pending = []
const Qt = { callLater(callback) { pending.push(callback) } }
const window = { shown: true, targetScreen: external, focusedScreen: () => screen }
screensChanged(window, { screens: [screen] }, Qt)
assertEqual(window.targetScreen, null, 'screen removal unmaps before Qt finishes destroying the old window')
pending.shift()()
assertEqual(window.targetScreen, screen, 'a showing overlay moves off a disconnected screen')
window.targetScreen = screen
window.focusedScreen = () => external
screensChanged(window, { screens: [screen, external] }, Qt)
assertEqual(window.targetScreen, screen, 'connecting another screen does not move a showing overlay')
window.shown = false
window.targetScreen = null
screensChanged(window, { screens: [screen, external] }, Qt)
assertEqual(window.targetScreen, null, 'screen changes leave a closed overlay unmapped')
window.shown = true
window.targetScreen = external
screensChanged(window, { screens: [screen] }, Qt)
window.shown = false
pending.shift()()
assertEqual(window.targetScreen, null, 'closing before a deferred remap prevents a stale reopen')
window.shown = true
window.targetScreen = external
screensChanged(window, { screens: [screen] }, Qt)
window.targetScreen = screen
window.focusedScreen = () => external
pending.shift()()
assertEqual(window.targetScreen, screen, 'a deferred remap preserves the screen chosen by a newer open')
const overlays = { const overlays = {
'shell/plugins/menu/Menu.qml': 'shown: root.opened && root.rowsLoaded', 'shell/plugins/menu/Menu.qml': 'shown: root.opened && root.rowsLoaded',
@@ -139,11 +48,11 @@ for (const [file, shown] of Object.entries(overlays)) {
const qml = read(file) const qml = read(file)
assert( assert(
qml.includes('OverlayWindow {') && qml.includes(shown) && !/PanelWindow \{\s*(id: panel\s*)?visible: root\.opened/.test(qml), qml.includes('OverlayWindow {') && qml.includes(shown) && !/PanelWindow \{\s*(id: panel\s*)?visible: root\.opened/.test(qml),
`${file} uses the shared overlay scale and lifecycle handling` `${file} keeps its surface through OverlayWindow`
) )
} }
// Layout resets on a requested close, independent of mapping readiness. // Window visibility never changes now, so close-time resets key off shown.
const menuQml = read('shell/plugins/menu/Menu.qml') const menuQml = read('shell/plugins/menu/Menu.qml')
assert( assert(
menuQml.includes('onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }') && menuQml.includes('onShownChanged: if (!shown) { cardTop = -1; maxRowsHeight = -1 }') &&
@@ -151,15 +60,9 @@ assert(
!/onVisibleChanged: if \(!visible\) \{ cardTop/.test(menuQml), !/onVisibleChanged: if \(!visible\) \{ cardTop/.test(menuQml),
'the menu unfreezes its layout when the overlay hides' 'the menu unfreezes its layout when the overlay hides'
) )
const screenHandler = /onTargetScreenChanged: \{([^\n]*)\}/.exec(menuQml)[1]
const changeScreen = new Function('panel', `with (panel) { ${screenHandler} }`)
const menuPanel = { shown: true, cardTop: 945, maxRowsHeight: 510 }
changeScreen(menuPanel)
assertEqual(menuPanel.cardTop, -1, 'moving an open menu off a disconnected output unfreezes its old top edge')
assertEqual(menuPanel.maxRowsHeight, -1, 'moving an open menu drops the old output row-height limit')
assert(menuPanel.shown, 'resetting the layout keeps the menu open')
// Fullscreen overlays open without an additional compositor animation. // A kept surface grows from its parked 1x1 when shown, which Hyprland animates
// as a slide in from the corner unless its layer rule turns animation off.
const shellRules = read('default/hypr/apps/omarchy-shell.lua') const shellRules = read('default/hypr/apps/omarchy-shell.lua')
const noAnim = /namespace = "\^\(([^)]*)\)\$" \}, no_anim = true/.exec(shellRules) const noAnim = /namespace = "\^\(([^)]*)\)\$" \}, no_anim = true/.exec(shellRules)
assert(noAnim, 'the shell overlays share one no-animation layer rule') assert(noAnim, 'the shell overlays share one no-animation layer rule')
@@ -1,36 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const read = name => fs.readFileSync(path.join(root, name), 'utf8')
const indicator = read('shell/plugins/bar/indicators/PasswordlessSudo.qml')
const widget = read('shell/plugins/bar/widgets/Indicators.qml')
const manifest = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json'))
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "Dictation"/), 'passwordless sudo is included in the default indicator tray')
assert(manifest.barWidget.schema.find(field => field.key === 'items').options.some(option => option.value === 'PasswordlessSudo'), 'passwordless sudo is configurable alongside Night Light')
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Color.urgent'), 'active passwordless sudo uses the theme danger color')
assertEqual(indicator.match(/activeText: "([^"]+)"/)[1], indicator.match(/inactiveText: "([^"]+)"/)[1], 'sudo keeps the same icon in both states')
assert(!widget.includes('sudoHorizontal') && !widget.includes('sudoVertical'), 'sudo participates in the normal indicator blocks')
assert(!indicator.includes('visible:'), 'sudo uses the shared indicator visibility and hover behavior')
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--active"]'), 'indicator uses the noninteractive grant probe')
assert(indicator.includes('root.granted = exitCode === 0 && exitStatus === 0'), 'failed or interrupted probes do not claim an active grant')
assert(indicator.includes('interval: 5000') && indicator.includes('onTriggered: root.refresh()'), 'indicator refreshes after activation, revocation, and expiry')
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--disable"]'), 'active indicator revokes access through a background process')
assert(indicator.includes('if (root.granted) disableProc.running = true'), 'active click bypasses the terminal launcher')
assert(indicator.includes('root.indicatorHost.refresh()'), 'disabling access refreshes all indicator instances immediately')
const press = new Function('root', 'disableProc', indicator.match(/onPressed: function\(\) \{([\s\S]*?)\n \}/)[1])
const launched = []
const button = { granted: true, bar: { run: command => launched.push(command) } }
const revoke = { running: false }
press(button, revoke)
assert(revoke.running && launched.length === 0, 'clicking active sudo starts background revocation without a terminal')
button.granted = false
press(button, revoke)
assert(launched.length === 0, 'repeat clicks during revocation cannot open the enable flow')
revoke.running = false
press(button, revoke)
assertDeepEqual(launched, ['omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless'], 'inactive click still opens interactive setup')
JS
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
export OMARCHY_PATH="$ROOT"
export TARGET_ROOT="$scratch/target root"
mkdir -p "$TARGET_ROOT/etc/omarchy"
apply() { "$ROOT/bin/omarchy-apply-pacman" "$1" "$TARGET_ROOT"; }
for channel in stable rc edge; do
apply "$channel"
cmp "$ROOT/default/pacman/pacman-$channel.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$ROOT/default/pacman/mirrorlist-$channel" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "missing region preserves all global channel templates byte for byte"
chmod 600 "$TARGET_ROOT/etc/pacman.conf"
apply stable
[[ $(stat -c %a "$TARGET_ROOT/etc/pacman.conf") == 600 ]] || fail "refresh keeps a customized pacman.conf mode"
chmod 644 "$TARGET_ROOT/etc/pacman.conf"
pass "refresh keeps existing config file modes"
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
for channel in stable rc edge; do
case "$channel" in
stable) host=stable-mirror.omarchy ;;
rc) host=rc-mirror.omarchy ;;
edge) host=mirror.omarchy ;;
esac
apply "$channel"
expected=$(printf 'Server = https://%s.cn/$repo/os/$arch\nServer = https://%s.org/$repo/os/$arch' "$host" "$host")
[[ $(<"$TARGET_ROOT/etc/pacman.d/mirrorlist") == "$expected" ]] || fail "$channel mirror order and variables"
{
cat "$ROOT/default/pacman/pacman-$channel.conf"
printf '\n[archlinuxcn]\nServer = https://mirrors.ustc.edu.cn/archlinuxcn/$arch\n'
} > "$scratch/expected.conf"
cmp "$scratch/expected.conf" "$TARGET_ROOT/etc/pacman.conf"
apply "$channel"
cmp "$TARGET_ROOT/etc/pacman.conf.bak" "$TARGET_ROOT/etc/pacman.conf"
cmp "$TARGET_ROOT/etc/pacman.d/mirrorlist.bak" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "China puts its channel mirror first, keeps the global one as fallback, and is idempotent"
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
cp "$TARGET_ROOT/etc/pacman.d/mirrorlist" "$scratch/before.mirrors"
for region in '' CN chn zz '../cn' '$(touch NEVER_EXECUTE)'; do
printf '%s\n' "$region" > "$TARGET_ROOT/etc/omarchy/region"
if apply stable > "$scratch/error" 2>&1; then
fail "invalid region accepted: $region"
fi
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
done
pass "unsupported and malformed regions fail before replacing either config"
printf 'cn\n' > "$TARGET_ROOT/etc/omarchy/region"
mkdir -p "$scratch/incomplete/default/regions/cn/pacman"
cp -a "$ROOT/default/pacman" "$scratch/incomplete/default/"
cp "$ROOT/default/regions/cn/pacman/pacman.conf.append" "$scratch/incomplete/default/regions/cn/pacman/"
if OMARCHY_PATH="$scratch/incomplete" apply stable > "$scratch/error" 2>&1; then
fail "incomplete region profile accepted"
fi
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$scratch/before.mirrors" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
pass "missing fragment cannot leave a half-generated config"
# Refresh renders these files unprivileged and hands root plain copies; its
# privileged path is covered in the sudo-boundary sandbox, never on the host.
cp "$TARGET_ROOT/etc/pacman.conf" "$scratch/before.conf"
mkdir -p "$scratch/rendered"
"$ROOT/bin/omarchy-apply-pacman" --render rc "$scratch/rendered" "$TARGET_ROOT"
rg -qxF 'Server = https://pkgs.omarchy.org/rc/$arch' "$scratch/rendered/pacman.conf"
rg -qxF '[archlinuxcn]' "$scratch/rendered/pacman.conf"
[[ $(<"$scratch/rendered/mirrorlist") == $'Server = https://rc-mirror.omarchy.cn/$repo/os/$arch\nServer = https://rc-mirror.omarchy.org/$repo/os/$arch' ]] ||
fail "render puts the regional channel mirror first"
cmp "$scratch/before.conf" "$TARGET_ROOT/etc/pacman.conf"
pass "render builds the regional channel files without touching the target"
if "$ROOT/bin/omarchy-apply-pacman" --render dev "$scratch/rendered" "$TARGET_ROOT" > "$scratch/error" 2>&1; then
fail "render accepts a nonexistent dev pacman template"
fi
pass "render rejects channels without pacman templates"
printf 'global\n' > "$TARGET_ROOT/etc/omarchy/region"
apply stable
cmp "$ROOT/default/pacman/pacman-stable.conf" "$TARGET_ROOT/etc/pacman.conf"
cmp "$ROOT/default/pacman/mirrorlist-stable" "$TARGET_ROOT/etc/pacman.d/mirrorlist"
pass "users can opt out of regional repository defaults"
# The shipped China profile is repository defaults only. Language, locale, and
# input methods follow the installer's language choice, not the region.
python3 - <<'PY'
import os
from pathlib import Path
region = Path(os.environ["ROOT"]) / "default/regions/cn"
packages = {line.strip() for line in (region / "packages").read_text().splitlines() if line.strip() and not line.startswith("#")}
assert packages == {"archlinuxcn-keyring"}, packages
assert not (region / "skel").exists()
PY
pass "China profile adds only its repository keyring"
# The installer derives the region from the chosen timezone, so every profile
# needs a mapping, and no timezone may select two regions.
python3 - <<'PY'
import os
from pathlib import Path
zoneinfo = Path("/usr/share/zoneinfo")
owners = {}
for profile in sorted((Path(os.environ["ROOT"]) / "default/regions").iterdir()):
lines = (profile / "timezones").read_text().splitlines()
zones = [line.strip() for line in lines if line.strip() and not line.startswith("#")]
assert zones, f"{profile.name} maps no timezones"
for zone in zones:
assert zone not in owners, f"{zone} maps to {owners[zone]} and {profile.name}"
owners[zone] = profile.name
if zoneinfo.is_dir():
assert (zoneinfo / zone).is_file(), f"{profile.name} lists unknown timezone {zone}"
# The installer offers tzdata's backward-compatible names too, so China
# covers every name that links to its two zones and nothing else.
# Without tzdata.zi (minimal containers) only the canonical zones can be
# checked, so completeness is enforced where the host has the link table.
china = {zone for zone, region in owners.items() if region == "cn"}
expected = {"Asia/Shanghai", "Asia/Urumqi"}
tzdata = zoneinfo / "tzdata.zi"
if tzdata.is_file():
for line in tzdata.read_text().splitlines():
fields = line.split()
if len(fields) == 3 and fields[0] == "L" and fields[1] in {"Asia/Shanghai", "Asia/Urumqi"}:
expected.add(fields[2])
assert china == expected, (china, expected)
else:
assert expected <= china, (china, expected)
PY
pass "region profiles map each timezone to at most one region"
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
for file in default/pacman/pacman-rc.conf default/pacman/mirrorlist-rc \
default/regions/cn/pacman/pacman.conf.append default/regions/cn/pacman/mirrorlist-rc.prepend; do
copy_boundary_file "$file"
done
# A sandboxed China machine: the render reads this region marker, never the
# host's /etc/omarchy/region.
mkdir -p "$SUDO_TEST_TARGET/etc/omarchy"
printf 'cn\n' >"$SUDO_TEST_TARGET/etc/omarchy/region"
# cp still logs its step, but now really copies, so the rendered files exist.
# Root's copies into /etc land in a capture directory instead of the host.
export SUDO_TEST_CAPTURE="$boundary_tmp/capture"
rm "$SUDO_TEST_ROOT/bin/cp"
cat >"$SUDO_TEST_ROOT/bin/cp" <<'STUB'
#!/bin/bash
set -euo pipefail
printf 'step:cp %s\n' "$*" >>"$SUDO_TEST_LOG"
destination=${!#}
if [[ $destination == /etc/* ]]; then
source=${*: -2:1}
[[ $source == /etc/* ]] && exit 0
/usr/bin/mkdir -p "$SUDO_TEST_CAPTURE${destination%/*}"
exec /usr/bin/cp "$source" "$SUDO_TEST_CAPTURE$destination"
fi
exec /usr/bin/cp "$@"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/cp"
# Regional rendering must stay unprivileged: root only copies finished files,
# exactly as the channel refresh did before regions existed.
reset_boundary
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1 || fail "refresh failed" "$(<"$boundary_tmp/output")"
assert_boundary_cold "regional refresh"
python3 - "$SUDO_TEST_LOG" "$SUDO_TEST_ROOT" <<'PY'
import re
import sys
events = open(sys.argv[1]).read().splitlines()
root = sys.argv[2]
render = [i for i, e in enumerate(events) if e.startswith('step:cp ' + root + '/default/pacman/pacman-rc.conf ')]
assert len(render) == 1, events
rendered = events[render[0]].split()[-1].rsplit('/', 1)[0]
copies = [i for i, e in enumerate(events) if e.startswith('sudo -N cp ')]
assert [events[i] for i in copies[2:]] == [
f'sudo -N cp -f {rendered}/pacman.conf /etc/pacman.conf',
f'sudo -N cp -f {rendered}/mirrorlist /etc/pacman.d/mirrorlist',
], events
assert not any(e.startswith('sudo ') and 'omarchy-apply-pacman' in e for e in events), events
assert render[0] < copies[0], events
transaction = next(i for i, e in enumerate(events) if e.startswith('step:pacman '))
assert max(copies) < transaction, events
PY
{
cat "$ROOT/default/pacman/pacman-rc.conf"
printf '\n'
cat "$ROOT/default/regions/cn/pacman/pacman.conf.append"
} >"$boundary_tmp/expected-pacman.conf"
cat "$ROOT/default/regions/cn/pacman/mirrorlist-rc.prepend" "$ROOT/default/pacman/mirrorlist-rc" >"$boundary_tmp/expected-mirrorlist"
cmp -s "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" ||
fail "root installs the rendered China pacman.conf" "$(diff "$boundary_tmp/expected-pacman.conf" "$SUDO_TEST_CAPTURE/etc/pacman.conf" 2>&1)"
cmp -s "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" ||
fail "root installs the rendered China mirrorlist" "$(diff "$boundary_tmp/expected-mirrorlist" "$SUDO_TEST_CAPTURE/etc/pacman.d/mirrorlist" 2>&1)"
pass "refresh renders regional config unprivileged and gives root only plain copies"
reset_boundary
if "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" dev >"$boundary_tmp/output" 2>&1; then
fail "refresh accepts an invalid channel"
fi
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
fail "invalid channel reached privileged work" "$(<"$SUDO_TEST_LOG")"
fi
pass "failed refresh does not proceed to privileged copies or a package update"
# A failed render still removes its scratch directory and exits cold.
reset_boundary
mv "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" "$boundary_tmp/omarchy-apply-pacman"
cat >"$SUDO_TEST_ROOT/bin/omarchy-apply-pacman" <<'STUB'
#!/bin/bash
: >"$3/pacman.conf"
exit 1
STUB
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
mkdir -p "$boundary_tmp/scratch"
if TMPDIR="$boundary_tmp/scratch" "$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" rc >"$boundary_tmp/output" 2>&1; then
fail "refresh succeeds after a failed render"
fi
mv "$boundary_tmp/omarchy-apply-pacman" "$SUDO_TEST_ROOT/bin/omarchy-apply-pacman"
[[ -z $(ls -A "$boundary_tmp/scratch") ]] || fail "failed render leaves its scratch directory" "$(ls -AR "$boundary_tmp/scratch")"
if grep -Eq '^(sudo -N cp |step:pacman )' "$SUDO_TEST_LOG"; then
fail "failed render reached privileged work" "$(<"$SUDO_TEST_LOG")"
fi
assert_boundary_cold "failed render"
pass "failed render removes its scratch directory and exits cold"
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
region_setup="$test_tmp/region.sh"
root="$test_tmp/root"
sed -e "s|/etc/|$root/etc/|g" -e "s| /\.cargo/| $root/.cargo/|g" "$ROOT/install/config/region.sh" >"$region_setup"
export OMARCHY_PATH="$ROOT"
mkdir -p "$root/etc/omarchy"
bash -e "$region_setup"
[[ -z $(find "$root" -type l) ]] || fail "no marker links nothing"
pass "no marker installs Global"
echo cn >"$root/etc/omarchy/region"
bash -e "$region_setup"
for link in "environment.conf etc/environment.d/90-omarchy-china.conf" "cargo.toml .cargo/config.toml" "gemrc etc/gemrc"; do
read -r source target <<<"$link"
[[ $(readlink "$root/$target") == "$ROOT/default/regions/cn/$source" ]] || fail "China links /$target to the shipped $source"
done
bash -e "$region_setup"
pass "the cn marker links the developer mirror drop-ins, again without error"
rm "$root/etc/gemrc"
echo ":sources: []" >"$root/etc/gemrc"
if output=$(bash -e "$region_setup" 2>&1); then
fail "China refuses to replace an admin's /etc/gemrc" "$output"
fi
[[ $output == "Refusing to replace $root/etc/gemrc, which Omarchy did not create" && $(<"$root/etc/gemrc") == ":sources: []" ]] ||
fail "China names the file it refuses to replace and leaves it" "$output"
rm "$root/etc/gemrc"
pass "region setup never replaces mirror config Omarchy did not create"
echo mars >"$root/etc/omarchy/region"
if output=$(bash -e "$region_setup" 2>&1); then
fail "an unknown region fails" "$output"
fi
[[ $output == "Unknown region: mars" ]] || fail "region setup names the unknown region" "$output"
pass "region setup rejects an unknown region"
grep -qx 'run_logged "$OMARCHY_INSTALL/config/region.sh"' "$ROOT/install/config/all.sh" || fail "the install runs region setup"
pass "the install runs region setup"
@@ -6,6 +6,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh" source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-update copy_boundary_file bin/omarchy-update
copy_boundary_file bin/omarchy-refresh-pacman copy_boundary_file bin/omarchy-refresh-pacman
copy_boundary_file bin/omarchy-apply-pacman
# Replace the step symlink, preserving the real fixture dispatcher. # Replace the step symlink, preserving the real fixture dispatcher.
rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs" rm "$SUDO_TEST_ROOT/bin/omarchy-update-aur-pkgs"
copy_boundary_file bin/omarchy-update-aur-pkgs copy_boundary_file bin/omarchy-update-aur-pkgs
+3 -25
View File
@@ -23,7 +23,7 @@ SH
} }
run_orphan_checker() { run_orphan_checker() {
HOME="$test_home" PATH="$stub_bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs" "$@" HOME="$test_home" PATH="$stub_bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs"
} }
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\nunused-tool\n"; exit 0; fi; exit 1' write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\nunused-tool\n"; exit 0; fi; exit 1'
@@ -35,29 +35,7 @@ grep -q '^ old-lib$' "$test_tmp/noninteractive.out" || fail "orphan checker lis
grep -q 'Re-run omarchy-update-orphan-pkgs in a terminal' "$test_tmp/noninteractive.out" || fail "orphan checker does not remove packages non-interactively" grep -q 'Re-run omarchy-update-orphan-pkgs in a terminal' "$test_tmp/noninteractive.out" || fail "orphan checker does not remove packages non-interactively"
pass "orphan checker only reports orphans non-interactively" pass "orphan checker only reports orphans non-interactively"
export REMOVAL_LOG="$test_tmp/removal.log"
write_stub pacman 'case $1 in
-Qtdq|-Qq) printf "old-lib\nunused-tool\n" ;;
-Rns) printf "%s\n" "$@" >"$REMOVAL_LOG" ;;
*) exit 1 ;;
esac'
write_stub sudo '"$@"'
run_orphan_checker -y >"$test_tmp/approved.out" 2>"$test_tmp/approved.err"
diff <(printf '%s\n' -Rns --noconfirm old-lib unused-tool) "$REMOVAL_LOG" ||
fail "approved cleanup removes the orphan packages without pacman confirmation"
pass "approved cleanup removes orphans without Omarchy or pacman confirmation"
write_stub sudo 'exit 42'
if run_orphan_checker -y >"$test_tmp/failure.out" 2>"$test_tmp/failure.err"; then
fail "approved cleanup reports removal failures"
fi
pass "approved cleanup reports removal failures"
rm -f "$REMOVAL_LOG"
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then exit 0; fi; exit 1' write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then exit 0; fi; exit 1'
for mode in "" -y; do run_orphan_checker >"$test_tmp/none.out" 2>"$test_tmp/none.err"
run_orphan_checker $mode >"$test_tmp/none.out" 2>"$test_tmp/none.err" [[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist"
[[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist"
done
[[ ! -e $REMOVAL_LOG ]] || fail "orphan checker does not remove packages when no orphans exist"
pass "orphan checker stays quiet without orphans" pass "orphan checker stays quiet without orphans"
-7
View File
@@ -35,9 +35,6 @@ for step in "${steps[@]}"; do
cat >"$stub_bin/$step" <<'STUB' cat >"$stub_bin/$step" <<'STUB'
#!/bin/bash #!/bin/bash
printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG" printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG"
if [[ ${0##*/} == "omarchy-update-orphan-pkgs" ]]; then
printf '%s\n' "$@" >"$STEP_LOG.orphan-args"
fi
[[ ${FAILING_STEP:-} != "${0##*/}" ]] || exit 1 [[ ${FAILING_STEP:-} != "${0##*/}" ]] || exit 1
STUB STUB
chmod +x "$stub_bin/$step" chmod +x "$stub_bin/$step"
@@ -87,8 +84,6 @@ run_update -y || fail "an update where everything works reports a failure"
diff <(expected_steps) <(steps_run) >"$test_tmp/order" || diff <(expected_steps) <(steps_run) >"$test_tmp/order" ||
fail "an update where everything works does not run every step in order" "$(cat "$test_tmp/order")" fail "an update where everything works does not run every step in order" "$(cat "$test_tmp/order")"
pass "an update where every step works runs all of them, in order" pass "an update where every step works runs all of them, in order"
[[ $(cat "$test_tmp/steps.orphan-args") == "-y" ]] || fail "an unattended update approves orphan removal"
pass "an unattended update approves orphan removal"
grep -q '^omarchy-update-system-pkgs unattended=1$' "$test_tmp/steps" || grep -q '^omarchy-update-system-pkgs unattended=1$' "$test_tmp/steps" ||
fail "-y does not mark the update unattended" fail "-y does not mark the update unattended"
@@ -98,8 +93,6 @@ diff <(expected_steps confirmed) <(steps_run) >"$test_tmp/order" ||
grep -q '^omarchy-update-system-pkgs unattended=$' "$test_tmp/steps" || grep -q '^omarchy-update-system-pkgs unattended=$' "$test_tmp/steps" ||
fail "an update a person confirmed is treated as unattended" fail "an update a person confirmed is treated as unattended"
pass "-y is what marks an update unattended, not the update itself" pass "-y is what marks an update unattended, not the update itself"
[[ $(cat "$test_tmp/steps.orphan-args") == "-y" ]] || fail "a confirmed update approves orphan removal"
pass "a confirmed update approves orphan removal"
# Migrations ship with the packages the upgrade installs and are written against # Migrations ship with the packages the upgrade installs and are written against
# them. Running them against what is still on disk is the failure this ordering # them. Running them against what is still on disk is the failure this ordering
-127
View File
@@ -1,127 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
leaf="$ROOT/install/hardware/dell-xps13-ptl-speaker-firmware.sh"
migration="$ROOT/migrations/1791265613.sh"
grep -q 'run_logged .*hardware/dell-xps13-ptl-speaker-firmware.sh' "$ROOT/install/hardware/all.sh" ||
fail "hardware setup installs the Panther Lake XPS 13 firmware"
pass "hardware setup installs the Panther Lake XPS 13 firmware"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin"
cat >"$test_tmp/bin/omarchy-hw-match" <<'SH'
#!/bin/bash
[[ $TEST_PRODUCT_NAME == *"$1"* ]]
SH
cat >"$test_tmp/bin/omarchy-hw-intel-ptl" <<'SH'
#!/bin/bash
[[ $TEST_INTEL_PTL == "1" ]]
SH
cat >"$test_tmp/bin/sudo" <<'SH'
#!/bin/bash
if [[ $1 == "pacman" ]]; then
exec "$@"
fi
printf 'Unexpected privileged command: %s\n' "$*" >&2
exit 1
SH
cat >"$test_tmp/bin/pacman" <<'SH'
#!/bin/bash
if [[ $1 == "-Q" ]]; then
if [[ ${@: -1} == "linux-firmware-cirrus-dx13260" ]]; then
[[ -e $TEST_ALIAS_FILE ]] || exit 1
printf 'linux-firmware-cirrus-dx13260 20260810-1\n'
else
[[ -s $TEST_VERSION_FILE ]] || exit 1
printf 'linux-firmware-cirrus %s\n' "$(<"$TEST_VERSION_FILE")"
fi
else
printf 'pacman %s\n' "$*" >>"$TEST_LOG"
[[ $* == "-S --noconfirm --needed -- linux-firmware-cirrus-dx13260" ]] || exit 1
[[ ${TEST_INSTALL_FAILURE:-0} == "0" ]] || exit 1
touch "$TEST_ALIAS_FILE"
fi
SH
cat >"$test_tmp/bin/omarchy-state" <<'SH'
#!/bin/bash
printf 'state %s\n' "$*" >>"$TEST_LOG"
SH
chmod +x "$test_tmp/bin/"*
export PATH="$test_tmp/bin:$ROOT/bin:$PATH"
export TEST_LOG="$test_tmp/calls" TEST_VERSION_FILE="$test_tmp/version" TEST_ALIAS_FILE="$test_tmp/alias-installed"
export TEST_PRODUCT_NAME="XPS 13 DX13260" TEST_INTEL_PTL=1
install_call="pacman -S --noconfirm --needed -- linux-firmware-cirrus-dx13260"
export OMARCHY_PATH="$ROOT"
reset_fixture() {
: >"$TEST_LOG"
printf '%s' "$1" >"$TEST_VERSION_FILE"
rm -f "$TEST_ALIAS_FILE"
}
run_leaf() {
bash -eE -c 'source "$1"' bash "$leaf"
}
run_migration() {
bash -euo pipefail "$migration" >/dev/null
}
for model in "XPS 9350" "XPS 13 DX13261"; do
reset_fixture "20260810-2"
TEST_PRODUCT_NAME="$model" run_leaf
TEST_PRODUCT_NAME="$model" run_migration
[[ ! -s $TEST_LOG ]] || fail "other models receive no firmware repair"
done
reset_fixture "20260810-2"
TEST_INTEL_PTL=0 run_leaf
TEST_INTEL_PTL=0 run_migration
[[ ! -s $TEST_LOG ]] || fail "the Wildcat Lake variant receives no firmware repair"
pass "other models and the Wildcat Lake variant receive no firmware repair"
for version in "" "20260810-2" "20260810-3" "20260810-4" "20260910-2" "20260916-1" "1:20260810-2"; do
reset_fixture "$version"
run_leaf || fail "the alias package is installed during hardware setup"
[[ $(<"$TEST_LOG") == "$install_call" && $(<"$TEST_VERSION_FILE") == "$version" && -e $TEST_ALIAS_FILE ]] ||
fail "hardware setup installs the separate aliases without replacing stock firmware"
done
pass "hardware setup installs the package identity without replacing any stock firmware"
pass "hardware setup leaves reboot bookkeeping to the migration"
reset_fixture "20260810-2"
touch "$TEST_ALIAS_FILE"
run_leaf
[[ ! -s $TEST_LOG ]] || fail "installed aliases are idempotent on stock firmware"
pass "installed aliases are idempotent on stock firmware"
run_migration
[[ $(<"$TEST_LOG") == "state set reboot-required" ]] ||
fail "the migration requests a reboot even if hardware setup already installed the aliases"
pass "the migration requests a reboot even if hardware setup already installed the aliases"
reset_fixture "20260810-2"
if TEST_INSTALL_FAILURE=1 run_migration; then
fail "a failed firmware installation fails the migration"
fi
[[ $(<"$TEST_LOG") == "$install_call" && $(<"$TEST_VERSION_FILE") == "20260810-2" ]] ||
fail "a failed installation leaves the migration retryable without claiming success"
pass "a failed firmware installation fails the migration and remains retryable"
: >"$TEST_LOG"
run_migration
[[ $(<"$TEST_LOG") == "$install_call"$'\nstate set reboot-required' ]] ||
fail "retrying the migration installs the firmware and requests a reboot"
pass "retrying the migration installs the firmware and requests a reboot"
: >"$TEST_LOG"
run_migration
[[ $(<"$TEST_LOG") == "state set reboot-required" ]] ||
fail "a second user before reboot is prompted without reinstalling"
pass "a second user before reboot is prompted without reinstalling"