#!/bin/bash # omarchy:summary=Lock before suspend and wait for the session lock to become secure # omarchy:group=system # omarchy:hidden=true # Overrunning the budget is the failure this whole path exists to prevent: # logind stops honouring the inhibitor and suspends mid-lock. Every call below # is bounded by what is left of the budget, so the deadline enforces itself # rather than depending on an estimate of how long a step ought to take. budget_cap_ms=12000 lock_timeout_ms=1000 status_timeout_ms=500 poll_interval=0.1 # logind decides how long a delay inhibitor may hold the machine, and the # shipped drop-in only counts once logind has reloaded it, so ask rather than # assume. Leaving logind a fifth of its own window to deliver PrepareForSleep # and act on the release gives 4s at the 5s default and 12s at the shipped 15s. # The cap keeps a hand-raised window from stranding a closed laptop in a bag. derive_budget_ms() { local window window=$(timeout --kill-after=0.1s 1s busctl get-property \ org.freedesktop.login1 /org/freedesktop/login1 \ org.freedesktop.login1.Manager InhibitDelayMaxUSec 2>/dev/null) window=${window##* } # An unreadable window means we cannot know, so assume logind's own default. [[ $window =~ ^[0-9]+$ ]] && (( window > 0 )) || window=5000000 window=$((window / 1000)) # Never leave logind less than a second, however small its window is. window=$((window - (window / 5 > 1000 ? window / 5 : 1000))) (( window < budget_cap_ms )) && echo "$window" || echo "$budget_cap_ms" } budget_ms=${1:-$(derive_budget_ms)} if [[ ! $budget_ms =~ ^[0-9]+$ ]] || (( budget_ms < 1 || budget_ms > budget_cap_ms )); then budget_ms=$(derive_budget_ms) fi # EPOCHREALTIME renders with the locale's decimal separator, so drop every # non-digit rather than assuming a period. A comma would otherwise read as # bash's comma operator and silently void the deadline. deadline_ms=$((10#${EPOCHREALTIME//[!0-9]/} / 1000 + budget_ms)) remaining_ms() { echo $((deadline_ms - 10#${EPOCHREALTIME//[!0-9]/} / 1000)) } # Clamping to what is left as well as to the call's own limit is what lets the # loop below stay a plain "while there is time" without predicting step costs. lock_ipc() { local limit=$1 remaining seconds shift remaining=$(remaining_ms) (( remaining > 0 )) || return 1 (( limit < remaining )) || limit=$remaining printf -v seconds '%d.%03d' $((limit / 1000)) $((limit % 1000)) OMARCHY_SHELL_IPC_TIMEOUT="$seconds" \ timeout --kill-after=0.1s "$seconds" omarchy-shell "$@" } # The shell answers refusals on stdout with a zero exit, so the reply is the # only way to spot a lock it can never perform. Nothing else needs inspecting: # the status poll is what confirms success, and re-requesting is idempotent, so # a request that may not have landed costs nothing to repeat. request_lock() { case $(lock_ipc "$lock_timeout_ms" lock lock 2>/dev/null) in missing-pam) report_unsecured "no lock screen is configured" ;; esac } # secure: done. locking: the shell has the request and is working on it, so # leave it alone. Anything else, unreadable replies included, means ask again. lock_state() { jq -r 'if .secure == true then "secure" elif .requested == true then "locking" else "idle" end' \ <<<"$(lock_ipc "$status_timeout_ms" lock status 2>/dev/null)" 2>/dev/null } sync_clamshell() { # Lid transitions can temporarily stall Hyprland IPC. This is best-effort: # the lid binding and monitor watcher also reconcile clamshell state. timeout --kill-after=0.1s 0.4s \ omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true } # logind suspends whether or not this wait succeeded, so a failure here means # the machine slept with the session exposed. The notification is the only way # anyone finds out, and it lands on the screen they unlock into. report_unsecured() { printf 'omarchy-system-sleep-lock: suspending without a secure lock (%s)\n' \ "$1" >&2 omarchy-notification-send -u critical -g 󰌾 \ "Screen did not lock before suspend" \ "The session was left unlocked ($1)." >/dev/null 2>&1 || true exit 1 } # Request the lock before touching monitor state, so a stuck Hyprland IPC call # cannot consume the window before Quickshell has begun securing the session. request_lock sync_clamshell # The trailing sleep can overshoot the deadline by one interval, which is well # inside the reserve derive_budget_ms already held back for logind. while (( $(remaining_ms) > 0 )); do case $(lock_state) in secure) exit 0 ;; locking) ;; *) request_lock ;; esac sleep "$poll_interval" done report_unsecured "the shell did not secure the session within ${budget_ms}ms"