Created
April 19, 2026 08:10
-
-
Save armenr/c186bcaa501235be36653f59d3d7c622 to your computer and use it in GitHub Desktop.
hyprland/hyprlock + Nvidia "Oopsie Daisy" fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # hypr-nvidia-fix.sh | |
| # | |
| # Originally posted: | |
| # | |
| # Check / apply / revert the fixes that make hyprlock recoverable after | |
| # suspend/resume on NVIDIA GPUs: | |
| # 1. Hyprland: misc:allow_session_lock_restore = true (sourced conf) | |
| # 2. NVIDIA modprobe: NVreg_PreserveVideoMemoryAllocations=1 | |
| # 3. systemd: nvidia-{suspend,hibernate,resume}.service enabled | |
| # 4. Initramfs: mkinitcpio -P (or dracut) so #2 actually takes effect | |
| # | |
| # Usage: hypr-nvidia-fix.sh {check|apply|revert} [--no-regen] | |
| # | |
| # Safe to re-run. Tracks its own changes in $STATE_DIR so revert only undoes | |
| # what this script applied; manually-configured state is left alone. | |
| set -euo pipefail | |
| # ---- Paths -------------------------------------------------------------- | |
| HYPR_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/hypr" | |
| HYPR_CONF="$HYPR_DIR/hyprland.conf" | |
| HYPR_FIX_CONF="$HYPR_DIR/conf/nvidia-session-lock-fix.conf" | |
| MODPROBE_CONF="/etc/modprobe.d/nvidia-power-management.conf" | |
| STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/hypr-nvidia-fix" | |
| BACKUP_DIR="$STATE_DIR/backups" | |
| SOURCE_LINE="source = $HYPR_FIX_CONF" | |
| SENTINEL_BEGIN="# hypr-nvidia-fix begin" | |
| SENTINEL_END="# hypr-nvidia-fix end" | |
| MODPROBE_LINE="options nvidia NVreg_PreserveVideoMemoryAllocations=1" | |
| NVIDIA_SERVICES=(nvidia-suspend.service nvidia-hibernate.service nvidia-resume.service) | |
| SUDO="" | |
| [[ $EUID -ne 0 ]] && SUDO="sudo" | |
| # ---- Output helpers ----------------------------------------------------- | |
| if [[ -t 1 ]]; then | |
| c_reset=$'\033[0m' | |
| c_bold=$'\033[1m' | |
| c_dim=$'\033[2m' | |
| c_green=$'\033[32m' | |
| c_yellow=$'\033[33m' | |
| c_red=$'\033[31m' | |
| c_cyan=$'\033[36m' | |
| else | |
| c_reset= | |
| c_bold= | |
| c_dim= | |
| c_green= | |
| c_yellow= | |
| c_red= | |
| c_cyan= | |
| fi | |
| step() { printf "\n${c_bold}${c_cyan}== %s ==${c_reset}\n" "$*"; } | |
| ok() { printf " ${c_green}[ OK ]${c_reset} %s\n" "$*"; } | |
| miss() { printf " ${c_yellow}[MISS]${c_reset} %s\n" "$*"; } | |
| add() { printf " ${c_green}[ADD ]${c_reset} %s\n" "$*"; } | |
| undo() { printf " ${c_yellow}[UNDO]${c_reset} %s\n" "$*"; } | |
| skip() { printf " ${c_dim}[SKIP]${c_reset} %s\n" "$*"; } | |
| warn() { printf " ${c_yellow}[WARN]${c_reset} %s\n" "$*"; } | |
| err() { printf " ${c_red}[ERR ]${c_reset} %s\n" "$*" >&2; } | |
| note() { printf " ${c_dim}%s${c_reset}\n" "$*"; } | |
| # ---- State markers ------------------------------------------------------ | |
| marker() { echo "$STATE_DIR/$1"; } | |
| mark_set() { mkdir -p "$STATE_DIR" && : > "$(marker "$1")"; } | |
| mark_exists() { [[ -e "$(marker "$1")" ]]; } | |
| mark_unset() { rm -f "$(marker "$1")"; } | |
| # ========================================================================= | |
| # 1. Hyprland: allow_session_lock_restore | |
| # ========================================================================= | |
| check_hyprland_setting() { | |
| local hit="" | |
| if [[ -d "$HYPR_DIR" ]]; then | |
| hit=$(grep -rhE '^[[:space:]]*allow_session_lock_restore[[:space:]]*=[[:space:]]*(true|1)[[:space:]]*$' \ | |
| "$HYPR_DIR" 2> /dev/null || true) | |
| fi | |
| if [[ -n "$hit" ]]; then | |
| ok "allow_session_lock_restore found in Hyprland config tree" | |
| note "match: $(printf '%s' "$hit" | head -n1 | sed 's/^[[:space:]]*//')" | |
| if [[ -f "$HYPR_CONF" ]] && grep -qF "$SOURCE_LINE" "$HYPR_CONF"; then | |
| ok "our source line is present in hyprland.conf" | |
| elif [[ -f "$HYPR_FIX_CONF" ]]; then | |
| warn "fix conf exists but hyprland.conf does not source it" | |
| fi | |
| return 0 | |
| fi | |
| miss "allow_session_lock_restore is NOT set anywhere in $HYPR_DIR" | |
| return 1 | |
| } | |
| apply_hyprland_setting() { | |
| if check_hyprland_setting > /dev/null 2>&1; then | |
| skip "Hyprland setting already configured" | |
| check_hyprland_setting || true | |
| return 0 | |
| fi | |
| if [[ ! -f "$HYPR_CONF" ]]; then | |
| err "Hyprland config not found at $HYPR_CONF — aborting this step" | |
| return 1 | |
| fi | |
| mkdir -p "$(dirname "$HYPR_FIX_CONF")" | |
| cat > "$HYPR_FIX_CONF" << 'EOF' | |
| # Managed by hypr-nvidia-fix.sh — do not hand-edit. | |
| # Lets the Hyprland "Oopsie daisy" screen re-spawn hyprlock cleanly after | |
| # a session-lock crash (common on NVIDIA after suspend/resume). | |
| # Ref: https://github.com/hyprwm/Hyprland/discussions/13184 | |
| misc { | |
| allow_session_lock_restore = true | |
| } | |
| EOF | |
| add "Created $HYPR_FIX_CONF" | |
| mark_set "hyprland_fix_conf_created" | |
| if ! grep -qF "$SOURCE_LINE" "$HYPR_CONF"; then | |
| mkdir -p "$BACKUP_DIR" | |
| cp -L --preserve=mode,timestamps "$HYPR_CONF" "$BACKUP_DIR/hyprland.conf" | |
| add "Backed up $HYPR_CONF -> $BACKUP_DIR/hyprland.conf" | |
| { | |
| echo "" | |
| echo "$SENTINEL_BEGIN" | |
| echo "$SOURCE_LINE" | |
| echo "$SENTINEL_END" | |
| } >> "$HYPR_CONF" | |
| add "Appended sourced line to $HYPR_CONF (between sentinel tags)" | |
| mark_set "hyprland_conf_modified" | |
| else | |
| skip "source line already in $HYPR_CONF" | |
| fi | |
| } | |
| revert_hyprland_setting() { | |
| if [[ -f "$HYPR_CONF" ]] && grep -qF "$SENTINEL_BEGIN" "$HYPR_CONF"; then | |
| # Remove block between sentinels inclusive | |
| sed -i "/$SENTINEL_BEGIN/,/$SENTINEL_END/d" "$HYPR_CONF" | |
| undo "Removed sentinel block from $HYPR_CONF" | |
| mark_unset "hyprland_conf_modified" | |
| elif mark_exists "hyprland_conf_modified"; then | |
| warn "marker says we edited $HYPR_CONF but sentinels are gone — not touching" | |
| note "backup available at: $BACKUP_DIR/hyprland.conf" | |
| else | |
| skip "no sentinel block in $HYPR_CONF — nothing to undo" | |
| fi | |
| if [[ -f "$HYPR_FIX_CONF" ]]; then | |
| if grep -q "Managed by hypr-nvidia-fix.sh" "$HYPR_FIX_CONF" 2> /dev/null; then | |
| rm -f "$HYPR_FIX_CONF" | |
| undo "Deleted $HYPR_FIX_CONF" | |
| mark_unset "hyprland_fix_conf_created" | |
| else | |
| warn "$HYPR_FIX_CONF exists but not managed by us — leaving alone" | |
| fi | |
| fi | |
| } | |
| # ========================================================================= | |
| # 2. NVIDIA modprobe: NVreg_PreserveVideoMemoryAllocations=1 | |
| # ========================================================================= | |
| check_modprobe() { | |
| local configured=1 | |
| if ! grep -qE '^[[:space:]]*options[[:space:]]+nvidia([[:space:]]+.*)?[[:space:]]+NVreg_PreserveVideoMemoryAllocations=1' \ | |
| /etc/modprobe.d/*.conf 2> /dev/null; then | |
| configured=0 | |
| fi | |
| if [[ $configured -eq 1 ]]; then | |
| ok "NVreg_PreserveVideoMemoryAllocations=1 configured in /etc/modprobe.d/" | |
| else | |
| miss "NVreg_PreserveVideoMemoryAllocations=1 NOT configured in /etc/modprobe.d/" | |
| fi | |
| # Runtime check is informational only — some driver versions don't expose | |
| # this parameter via sysfs even when the option is active. The modprobe.d | |
| # file is the source of truth; initramfs regen makes it take effect. | |
| local runtime="/sys/module/nvidia/parameters/NVreg_PreserveVideoMemoryAllocations" | |
| if [[ -r "$runtime" ]]; then | |
| local val | |
| val=$(cat "$runtime" 2> /dev/null || echo "?") | |
| case "$val" in | |
| Y | 1) ok "runtime sysfs: active ($val)" ;; | |
| N | 0) note "runtime sysfs: $val — regen initramfs and reboot to apply" ;; | |
| *) note "runtime sysfs: value is '$val'" ;; | |
| esac | |
| else | |
| note "runtime sysfs: param not exposed (normal for some driver versions)" | |
| fi | |
| [[ $configured -eq 1 ]] | |
| } | |
| apply_modprobe() { | |
| if check_modprobe > /dev/null 2>&1; then | |
| skip "modprobe option already set" | |
| check_modprobe || true | |
| return 0 | |
| fi | |
| mkdir -p "$BACKUP_DIR" | |
| if [[ -f "$MODPROBE_CONF" ]]; then | |
| $SUDO cp -L --preserve=mode,ownership,timestamps "$MODPROBE_CONF" \ | |
| "$BACKUP_DIR/nvidia-power-management.conf" | |
| add "Backed up $MODPROBE_CONF" | |
| mark_set "modprobe_existed_before" | |
| fi | |
| add "Writing $MODPROBE_CONF (requires $SUDO)" | |
| printf '%s\n' "$MODPROBE_LINE" | $SUDO tee "$MODPROBE_CONF" > /dev/null | |
| mark_set "modprobe_written" | |
| note "initramfs needs regen to bake this into the early-boot image" | |
| note "step 4 handles that unless you passed --no-regen" | |
| } | |
| # ========================================================================= | |
| # 4. Initramfs regeneration | |
| # ========================================================================= | |
| # Runs the same command pacman runs automatically on kernel/nvidia updates. | |
| # No-op if the modprobe step didn't actually change anything. | |
| detect_initramfs_tool() { | |
| if command -v mkinitcpio > /dev/null 2>&1; then | |
| echo "mkinitcpio" | |
| elif command -v dracut > /dev/null 2>&1; then | |
| echo "dracut" | |
| else | |
| echo "none" | |
| fi | |
| } | |
| check_initramfs() { | |
| local tool | |
| tool=$(detect_initramfs_tool) | |
| case "$tool" in | |
| mkinitcpio) ok "mkinitcpio detected ($(mkinitcpio --version 2> /dev/null | awk '{print $2}' | head -1))" ;; | |
| dracut) ok "dracut detected" ;; | |
| none) miss "no supported initramfs generator found" ;; | |
| esac | |
| if [[ -f "$STATE_DIR/last_regen_at" ]]; then | |
| note "last regen by this script: $(cat "$STATE_DIR/last_regen_at")" | |
| fi | |
| } | |
| apply_initramfs_regen() { | |
| local tool | |
| tool=$(detect_initramfs_tool) | |
| if [[ "$tool" == "none" ]]; then | |
| err "no initramfs tool found — skipping" | |
| note "install mkinitcpio (or dracut) and regen manually before reboot" | |
| return 1 | |
| fi | |
| if [[ "${SKIP_REGEN:-0}" == "1" ]]; then | |
| warn "--no-regen passed; skipping initramfs regen" | |
| note "REMEMBER: sudo $tool $([[ $tool == mkinitcpio ]] && echo '-P' || echo '--regenerate-all --force')" | |
| note "must be run before reboot or the modprobe option will not apply" | |
| return 0 | |
| fi | |
| # Skip if nothing actually changed in this session AND a prior regen is recorded | |
| if ! mark_exists "modprobe_written" && [[ -f "$STATE_DIR/last_regen_at" ]]; then | |
| skip "modprobe unchanged and a prior regen is on record" | |
| return 0 | |
| fi | |
| local logfile="$STATE_DIR/${tool}.log" | |
| add "Running: sudo $tool (logging to $logfile)" | |
| note "this takes 30-90s and may print firmware/hook warnings — normal" | |
| local rc=0 | |
| case "$tool" in | |
| mkinitcpio) | |
| $SUDO mkinitcpio -P 2>&1 | tee "$logfile" | |
| rc="${PIPESTATUS[0]}" | |
| ;; | |
| dracut) | |
| $SUDO dracut --regenerate-all --force 2>&1 | tee "$logfile" | |
| rc="${PIPESTATUS[0]}" | |
| ;; | |
| esac | |
| if [[ "$rc" -eq 0 ]]; then | |
| ok "initramfs regenerated successfully" | |
| date -Iseconds > "$STATE_DIR/last_regen_at" | |
| mark_set "initramfs_regenerated" | |
| else | |
| err "$tool exited with status $rc" | |
| note "prior initramfs image is still in place; system remains bootable" | |
| note "review the tail of: $logfile" | |
| return 1 | |
| fi | |
| } | |
| revert_initramfs_regen() { | |
| local tool | |
| tool=$(detect_initramfs_tool) | |
| if [[ "$tool" == "none" ]]; then | |
| skip "no initramfs tool — nothing to regen" | |
| return 0 | |
| fi | |
| if [[ "${SKIP_REGEN:-0}" == "1" ]]; then | |
| warn "--no-regen passed; skipping regen on revert" | |
| note "run the regen manually before next reboot to complete the revert" | |
| return 0 | |
| fi | |
| # Only regen on revert if we actually changed the modprobe file | |
| if ! mark_exists "modprobe_written" 2> /dev/null && [[ ! -f "$STATE_DIR/last_regen_at" ]]; then | |
| # modprobe_written marker was cleared by revert_modprobe already; check if | |
| # this run actually reverted anything observable by checking last_regen_at | |
| : | |
| fi | |
| local logfile="$STATE_DIR/${tool}.revert.log" | |
| add "Running: sudo $tool to bake the revert into initramfs (logging to $logfile)" | |
| local rc=0 | |
| case "$tool" in | |
| mkinitcpio) | |
| $SUDO mkinitcpio -P 2>&1 | tee "$logfile" | |
| rc="${PIPESTATUS[0]}" | |
| ;; | |
| dracut) | |
| $SUDO dracut --regenerate-all --force 2>&1 | tee "$logfile" | |
| rc="${PIPESTATUS[0]}" | |
| ;; | |
| esac | |
| if [[ "$rc" -eq 0 ]]; then | |
| ok "initramfs regenerated after revert" | |
| mark_unset "initramfs_regenerated" | |
| else | |
| err "$tool exited with status $rc — prior initramfs still in place" | |
| note "review the tail of: $logfile" | |
| fi | |
| } | |
| revert_modprobe() { | |
| if ! mark_exists "modprobe_written"; then | |
| skip "no modprobe change to revert" | |
| return 0 | |
| fi | |
| if mark_exists "modprobe_existed_before" && [[ -f "$BACKUP_DIR/nvidia-power-management.conf" ]]; then | |
| $SUDO cp -L --preserve=mode,ownership,timestamps \ | |
| "$BACKUP_DIR/nvidia-power-management.conf" "$MODPROBE_CONF" | |
| undo "Restored $MODPROBE_CONF from backup" | |
| else | |
| $SUDO rm -f "$MODPROBE_CONF" | |
| undo "Removed $MODPROBE_CONF (we created it)" | |
| fi | |
| mark_unset "modprobe_written" | |
| mark_unset "modprobe_existed_before" | |
| warn "Regenerate initramfs and reboot to fully restore previous kernel module state" | |
| } | |
| # ========================================================================= | |
| # 3. NVIDIA systemd services | |
| # ========================================================================= | |
| check_services() { | |
| local all=1 | |
| for svc in "${NVIDIA_SERVICES[@]}"; do | |
| if ! systemctl list-unit-files "$svc" > /dev/null 2>&1; then | |
| miss "$svc not installed on this system" | |
| all=0 | |
| continue | |
| fi | |
| if systemctl is-enabled --quiet "$svc" 2> /dev/null; then | |
| ok "$svc is enabled" | |
| else | |
| local state | |
| state=$(systemctl is-enabled "$svc" 2> /dev/null || true) | |
| [[ -z "$state" ]] && state="unknown" | |
| miss "$svc is NOT enabled (state: $state)" | |
| all=0 | |
| fi | |
| done | |
| [[ $all -eq 1 ]] | |
| } | |
| apply_services() { | |
| for svc in "${NVIDIA_SERVICES[@]}"; do | |
| if ! systemctl list-unit-files "$svc" > /dev/null 2>&1; then | |
| err "$svc is not installed — skipping (install nvidia-utils?)" | |
| continue | |
| fi | |
| if systemctl is-enabled --quiet "$svc" 2> /dev/null; then | |
| skip "$svc already enabled" | |
| continue | |
| fi | |
| add "Enabling $svc (requires $SUDO)" | |
| if $SUDO systemctl enable "$svc"; then | |
| mark_set "service_enabled.$svc" | |
| else | |
| err "Failed to enable $svc" | |
| fi | |
| done | |
| } | |
| revert_services() { | |
| for svc in "${NVIDIA_SERVICES[@]}"; do | |
| if ! mark_exists "service_enabled.$svc"; then | |
| skip "we did not enable $svc — leaving alone" | |
| continue | |
| fi | |
| undo "Disabling $svc (requires $SUDO)" | |
| if $SUDO systemctl disable "$svc"; then | |
| mark_unset "service_enabled.$svc" | |
| else | |
| err "Failed to disable $svc" | |
| fi | |
| done | |
| } | |
| # ========================================================================= | |
| # Dispatch | |
| # ========================================================================= | |
| cmd_check() { | |
| printf '%sHyprland + NVIDIA session-lock fix — status%s\n' "$c_bold" "$c_reset" | |
| note "state dir: $STATE_DIR" | |
| step "1. Hyprland: misc:allow_session_lock_restore" | |
| check_hyprland_setting || true | |
| step "2. NVIDIA modprobe: NVreg_PreserveVideoMemoryAllocations=1" | |
| check_modprobe || true | |
| step "3. NVIDIA suspend/resume systemd services" | |
| check_services || true | |
| step "4. Initramfs generator" | |
| check_initramfs || true | |
| if [[ -f "$STATE_DIR/applied_at" ]]; then | |
| printf "\n" | |
| note "last apply run: $(cat "$STATE_DIR/applied_at")" | |
| fi | |
| } | |
| cmd_apply() { | |
| printf '%sHyprland + NVIDIA session-lock fix — applying%s\n' "$c_bold" "$c_reset" | |
| mkdir -p "$STATE_DIR" "$BACKUP_DIR" | |
| note "state dir: $STATE_DIR" | |
| [[ "${SKIP_REGEN:-0}" == "1" ]] && note "regen: DISABLED via --no-regen" | |
| step "1. Hyprland: misc:allow_session_lock_restore" | |
| apply_hyprland_setting | |
| step "2. NVIDIA modprobe: NVreg_PreserveVideoMemoryAllocations=1" | |
| apply_modprobe | |
| step "3. NVIDIA suspend/resume systemd services" | |
| apply_services | |
| step "4. Regenerate initramfs" | |
| apply_initramfs_regen || true | |
| date -Iseconds > "$STATE_DIR/applied_at" | |
| printf '\n%sDone.%s Next steps:\n' "$c_bold" "$c_reset" | |
| note "1. Reload Hyprland: hyprctl reload" | |
| note "2. Reboot to activate the kernel module option" | |
| note "3. Verify afterwards: $0 check" | |
| } | |
| cmd_revert() { | |
| printf '%sHyprland + NVIDIA session-lock fix — reverting%s\n' "$c_bold" "$c_reset" | |
| if [[ ! -d "$STATE_DIR" ]]; then | |
| warn "no state dir at $STATE_DIR — nothing to revert (or state lost)" | |
| return 0 | |
| fi | |
| note "state dir: $STATE_DIR" | |
| [[ "${SKIP_REGEN:-0}" == "1" ]] && note "regen: DISABLED via --no-regen" | |
| step "1. Hyprland: misc:allow_session_lock_restore" | |
| revert_hyprland_setting | |
| step "2. NVIDIA modprobe" | |
| revert_modprobe | |
| step "3. NVIDIA suspend/resume systemd services" | |
| revert_services | |
| step "4. Regenerate initramfs (to bake in reverted modprobe)" | |
| revert_initramfs_regen || true | |
| printf '\n%sRevert complete.%s\n' "$c_bold" "$c_reset" | |
| note "Reload Hyprland: hyprctl reload" | |
| note "Reboot to fully restore previous kernel module state" | |
| note "Backups kept in: $BACKUP_DIR" | |
| } | |
| usage() { | |
| cat << EOF | |
| Usage: $(basename "$0") {check|apply|revert} [--no-regen] | |
| check Read-only status of all 4 fixes | |
| apply Apply missing fixes; originals backed up to: | |
| $BACKUP_DIR | |
| revert Undo exactly what a previous apply did (uses state markers | |
| and sentinel tags so manual config is never touched) | |
| --no-regen Skip the initramfs regen step in apply/revert. You MUST run | |
| 'sudo mkinitcpio -P' (or equivalent) yourself before reboot. | |
| State: $STATE_DIR | |
| EOF | |
| } | |
| # ---- Parse args ---- | |
| SKIP_REGEN=0 | |
| CMD="" | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --no-regen) SKIP_REGEN=1 ;; | |
| check | apply | revert) CMD="$arg" ;; | |
| -h | --help | "") | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| usage | |
| exit 2 | |
| ;; | |
| esac | |
| done | |
| case "$CMD" in | |
| check) cmd_check ;; | |
| apply) cmd_apply ;; | |
| revert) cmd_revert ;; | |
| "") | |
| usage | |
| exit 2 | |
| ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment