Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
466c2ca14a
|
||
|
|
ec4579a461 | ||
|
|
d9a781e3c9 | ||
|
|
a6c3480ff8 | ||
|
|
545190596d | ||
|
|
b381093e32 | ||
|
|
9d74c813f1 | ||
|
|
fce85abf48 | ||
|
|
3c68a1bb04 | ||
|
|
b89def6c7f | ||
|
|
0de49b231a | ||
|
|
3e00a48c00 | ||
|
|
a1bdc46119 | ||
|
|
7a60c8dea4 | ||
|
|
6be88d14a5 | ||
|
|
50a26be6a7 | ||
|
|
d5ef1c1d15 | ||
|
|
d024c82a9d | ||
|
|
d7f17a0424 | ||
|
|
87a2931209 | ||
|
|
647979bde7 | ||
|
|
123535ea14 | ||
|
|
645cfc2a36 | ||
|
|
e989960895 | ||
|
|
48f7c411c0 | ||
|
|
7727d40c92 |
No files matched your search
@@ -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/`
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+77
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[source.crates-io]
|
||||||
|
replace-with = "aliyun"
|
||||||
|
|
||||||
|
[source.aliyun]
|
||||||
|
registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
:sources:
|
||||||
|
- https://mirrors.aliyun.com/rubygems/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<settings>
|
||||||
|
<mirrors>
|
||||||
|
<mirror>
|
||||||
|
<id>aliyun</id>
|
||||||
|
<mirrorOf>central</mirrorOf>
|
||||||
|
<url>https://maven.aliyun.com/repository/public</url>
|
||||||
|
</mirror>
|
||||||
|
</mirrors>
|
||||||
|
</settings>
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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,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"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: ({})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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) &&
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
Executable
+101
@@ -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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
Reference in new issue
Block a user