Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bin/omarchy-hibernation-remove
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ if grep -Fq "$SWAP_FILE" /etc/fstab; then
sudo sed -i '/^# Btrfs swapfile for system hibernation$/d' /etc/fstab
fi

# Without hibernation, UPower's critical action goes back to its default
UPOWER_DROP_IN="/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf"
if [[ -f $UPOWER_DROP_IN ]]; then
echo "Removing critical battery hibernation"
sudo rm -f "$UPOWER_DROP_IN"
sudo systemctl try-restart upower.service
fi

# Remove mkinitcpio resume hook
echo "Removing resume hook"
sudo rm "$MKINITCPIO_CONF"
Expand Down
38 changes: 37 additions & 1 deletion bin/omarchy-hibernation-setup
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ MKINITCPIO_CONF="/etc/mkinitcpio.conf.d/omarchy_resume.conf"
SWAP_FILE="/swap/swapfile"
RESUME_DROP_IN="/etc/limine-entry-tool.d/resume.conf"
RESUME_REBUILD_PENDING="/var/lib/omarchy/hibernation-resume-rebuild"
UPOWER_DROP_IN="/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf"

install_root_file() {
local source="$1"
Expand Down Expand Up @@ -137,6 +138,34 @@ rebuild_renamed_resume_device() {
return 1
}

# The kernel can only restore a hibernated session once it knows where the
# image is: the resume parameters carry the swapfile's offset.
resume_ready() {
[[ -f $RESUME_DROP_IN ]] && grep -q 'resume_offset=' "$RESUME_DROP_IN" && ! grep -q 'resume_offset="$' "$RESUME_DROP_IN"
}

# On a laptop, hibernate when the battery runs critically low instead of
# suspending into it. Rerunning setup installs it on machines set up before.
# Only once resume works, so a dying battery never powers off a session that
# cannot be restored.
setup_critical_battery_hibernate() {
omarchy-battery-present || return 0
if ! resume_ready; then
echo "Not hibernating on critical battery until the resume offset is set" >&2
return 0
fi
cmp -s "$OMARCHY_PATH/default/upower/omarchy-critical-hibernate.conf" "$UPOWER_DROP_IN" && return 0

echo "Hibernating on critical battery"
sudo mkdir -p "${UPOWER_DROP_IN%/*}"
if install_root_file "$OMARCHY_PATH/default/upower/omarchy-critical-hibernate.conf" "$UPOWER_DROP_IN" 0644; then
sudo systemctl try-restart upower.service
else
echo "Could not install the critical battery action" >&2
return 1
fi
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
}

# Check if hibernation is already configured
if [[ -f $MKINITCPIO_CONF ]] && grep -q "^HOOKS+=(resume)$" "$MKINITCPIO_CONF"; then
# Fix empty resume_offset if btrfs map-swapfile failed during initial setup
Expand All @@ -154,6 +183,7 @@ if [[ -f $MKINITCPIO_CONF ]] && grep -q "^HOOKS+=(resume)$" "$MKINITCPIO_CONF";
# After the offset repair, which leaves the device as it found it.
rename_resume_device || exit 1
rebuild_renamed_resume_device || exit 1
setup_critical_battery_hibernate || exit 1
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
echo "Hibernation is already set up"
exit 0
fi
Expand Down Expand Up @@ -233,16 +263,22 @@ if grep -q "\[s2idle\]" /sys/power/mem_sleep 2>/dev/null; then
fi
fi

rebuilt=true
if ! $NO_REBUILD; then
# limine-mkinitcpio rebuilds initramfs/UKI for all kernels and updates the
# /boot/limine.conf entries via limine-entry-tool. The limine bootloader
# binary on the ESP doesn't change here, so we don't need limine-update
# (which would also re-deploy the binary and rebuild a second time).
echo "Regenerating initramfs..."
sudo limine-mkinitcpio
sudo limine-mkinitcpio || rebuilt=false
echo
fi

# With --no-rebuild the caller rebuilds, so the resume parameters land then.
if $rebuilt; then
setup_critical_battery_hibernate
fi

if ! $FORCE && ! $NO_REBUILD && gum confirm "Reboot to enable hibernation?"; then
omarchy-system reboot
fi
12 changes: 12 additions & 0 deletions default/upower/omarchy-critical-hibernate.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Installed by omarchy-hibernation-setup on laptops. UPower's Auto action is
# logind's Sleep(), which is suspend-then-hibernate: at the last few percent the
# suspend can drain the battery before the hibernate ever runs. Hibernate
# directly, early enough for the image to be written on battery.
#
# To pick another level, put PercentageAction under [UPower] in a later drop-in,
# such as /etc/UPower/UPower.conf.d/99-local.conf, and keep it below
# PercentageCritical. Setup leaves that file alone.
[UPower]
CriticalPowerAction=Hibernate
PercentageCritical=8.0
PercentageAction=5.0
2 changes: 2 additions & 0 deletions manual/36-system-sleep.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ You toggle suspend by running `omarchy toggle suspend` from the terminal. That j
You set up hibernation by running `omarchy hibernation setup` from the terminal. Hibernation creates a /swap subvolume on your boot drive the size of your physical RAM allocation, so make sure you have plenty of room to spare. On a 32GB machine, you'll always need 32GB+ free for this volume. Hibernation also requires the default Limine bootloader.

When set up, you'll see the hibernate option under _System_ (or `Super + Esc`), and then you can see if it works consistently on your system. If not, you can remove it again by running `omarchy hibernation remove`.

On a laptop, setup also makes the machine hibernate when the battery reaches 5% instead of suspending with a nearly empty battery. To pick another level, put `PercentageAction=3.0` under `[UPower]` in `/etc/UPower/UPower.conf.d/99-local.conf` (keep it below `PercentageCritical`, which is 8), then run `sudo systemctl restart upower`.
12 changes: 12 additions & 0 deletions migrations/1793289056.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
echo "Hibernate on critical battery on laptops that already set up hibernation"

# omarchy-hibernation-setup now installs a UPower drop-in that hibernates on
# critical battery, but only when it runs. A laptop set up before keeps UPower's
# Auto action, which suspends into a dying battery (#13256). On a machine that
# already has hibernation, setup skips the swap and boot steps and only adds
# what is missing, and it leaves an installed drop-in alone.
resume_conf="${OMARCHY_RESUME_CONF:-/etc/mkinitcpio.conf.d/omarchy_resume.conf}"

if [[ -f $resume_conf ]] && grep -q "^HOOKS+=(resume)$" "$resume_conf" && omarchy-battery-present; then
omarchy-hibernation-setup
fi
105 changes: 105 additions & 0 deletions test/shell.d/hibernation-critical-battery-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/bin/bash
#
# Hibernation setup makes a laptop hibernate on critical battery, and removing
# hibernation takes that back. /etc paths are redirected into a scratch root;
# sudo runs the file operations there and records service restarts.
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

set -euo pipefail

source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"

scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
mkdir -p "$scratch/bin" "$scratch/etc/mkinitcpio.conf.d" "$scratch/etc/limine-entry-tool.d"
: > "$scratch/image_size"
export CALL_LOG="$scratch/calls"
export OMARCHY_PATH="$ROOT"
# omarchy-sleep-disabled reads this, so a host with sleep turned off doesn't skip setup.
export OMARCHY_SLEEP_CONFIG_DIR="$scratch/etc/systemd/sleep.conf.d"
export PATH="$scratch/bin:$ROOT/bin:$PATH"

cat > "$scratch/bin/sudo" <<'STUB'
#!/bin/bash
case "$1" in
systemctl) echo "systemctl ${*:2}" >> "$CALL_LOG" ;;
/usr/bin/install)
args=()
while (($# > 1)); do
shift
case "$1" in -o | -g) shift ;; *) args+=("$1") ;; esac
done
exec /usr/bin/install "${args[@]}"
;;
*) exec "$@" ;;
esac
STUB
printf '#!/bin/bash\n[[ ${BATTERY:-1} == 1 ]]\n' > "$scratch/bin/omarchy-battery-present"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
printf '#!/bin/bash\nexit 0\n' > "$scratch/bin/gum"
# Setup skips Apple Silicon, so the test must not ask the host's hardware.
printf '#!/bin/bash\nexit 1\n' > "$scratch/bin/omarchy-hw-aarch64-apple"
printf '#!/bin/bash\necho "btrfs $*" >> "$CALL_LOG"\nexit 1\n' > "$scratch/bin/btrfs"
chmod +x "$scratch/bin/"*

redirect() {
sed -e "s|/etc/|$scratch/etc/|g" -e "s|/var/lib/omarchy/|$scratch/var/lib/omarchy/|g" -e "s|\"/swap|\"$scratch/swap|g" \
-e "s|/sys/power/image_size|$scratch/image_size|g" "$ROOT/bin/$1" > "$scratch/$1"
}
redirect omarchy-hibernation-setup
redirect omarchy-hibernation-remove

drop_in="$scratch/etc/UPower/UPower.conf.d/70-omarchy-critical-hibernate.conf"
resume_params="$scratch/etc/limine-entry-tool.d/resume.conf"
echo "HOOKS+=(resume)" > "$scratch/etc/mkinitcpio.conf.d/omarchy_resume.conf"

# Without a resume offset the kernel cannot restore the image, so a dying
# battery would power off a session for good. The action waits for the offset.
echo 'KERNEL_CMDLINE[default]+=" resume=/dev/root resume_offset="' > "$resume_params"
bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1 ||
fail "setup without a resume offset still succeeds" "$(<"$scratch/out")"
[[ ! -e $drop_in ]] || fail "a laptop without a resume offset gets no critical battery action"
rm -f "$resume_params"
bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1
[[ ! -e $drop_in ]] || fail "a laptop without resume parameters gets no critical battery action"
pass "the critical battery action waits until resume can restore the session"

echo 'KERNEL_CMDLINE[default]+=" resume=/dev/root resume_offset=533760"' > "$resume_params"

: > "$CALL_LOG"
bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1 ||
fail "setup on an already-hibernating laptop succeeds" "$(<"$scratch/out")"
cmp -s "$ROOT/default/upower/omarchy-critical-hibernate.conf" "$drop_in" ||
fail "a laptop set up earlier gets the critical battery action" "$(<"$scratch/out")"
grep -qx 'systemctl try-restart upower.service' "$CALL_LOG" || fail "UPower is restarted to read the new action"
pass "rerunning setup on a laptop installs the critical battery hibernate action"

grep -qx 'CriticalPowerAction=Hibernate' "$drop_in" || fail "the critical action is Hibernate"
pass "the critical battery action is Hibernate, not suspend-then-hibernate"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved

: > "$CALL_LOG"
bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1
[[ ! -s $CALL_LOG ]] || fail "an unchanged drop-in does not restart UPower" "$(<"$CALL_LOG")"
pass "an installed action is left alone"

rm -rf "$scratch/etc/UPower"
BATTERY=0 bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1
[[ ! -e $drop_in ]] || fail "a desktop gets no battery action"
pass "a machine without a battery gets no battery action"

# The migration runs setup on laptops set up before; a failed install has to
# fail setup so the migration runs again instead of being marked done.
mkdir -p "$scratch/etc/UPower" && touch "$scratch/etc/UPower/UPower.conf.d"
if bash "$scratch/omarchy-hibernation-setup" --no-rebuild > "$scratch/out" 2>&1; then
fail "setup reports a failed install of the critical battery action" "$(<"$scratch/out")"
fi
rm -rf "$scratch/etc/UPower"
pass "a failed install fails setup so the migration retries"

bash "$scratch/omarchy-hibernation-setup" --no-rebuild > /dev/null 2>&1
printf '#!/bin/bash\nexit 0\n' > "$scratch/bin/limine-mkinitcpio"
printf '#!/bin/bash\nexit 1\n' > "$scratch/bin/swapon"
chmod +x "$scratch/bin/limine-mkinitcpio" "$scratch/bin/swapon"
: > "$CALL_LOG"
bash "$scratch/omarchy-hibernation-remove" > "$scratch/out" 2>&1 || fail "remove succeeds" "$(<"$scratch/out")"
[[ ! -e $drop_in ]] || fail "removing hibernation removes the critical battery action"
grep -qx 'systemctl try-restart upower.service' "$CALL_LOG" || fail "UPower is restarted after the action is removed"
pass "removing hibernation removes the critical battery action"