#!/usr/bin/env bash set -uo pipefail LOGTAG=revive-monitor-fix SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # Source-tree and installed layouts. COMMON="$SCRIPT_DIR/../lib/hyprland-hdmi-revive/common.sh" [[ -f "$COMMON" ]] || COMMON="$SCRIPT_DIR/../lib/common.sh" # shellcheck source=lib/common.sh source "$COMMON" || exit 1 load_config || exit 1 require_commands timeout loginctl hyprctl jq sudo flock stat sleep || exit 1 prepare_runtime || exit 1 GRAPHICAL_VT="${1:-}" if [[ ! "$GRAPHICAL_VT" =~ ^[1-9][0-9]?$ ]] || (( GRAPHICAL_VT > 63 )); then log 'No valid graphical VT supplied (expected 1..63)' exit 1 fi exec 9>"$STATE_DIR/fix.lock" || exit 1 flock -n -E 75 9 lock_status=$? if (( lock_status == 75 )); then log 'Recovery already running - skipping' exit 0 elif (( lock_status != 0 )); then log 'Cannot acquire recovery lock' exit 1 fi SESSION="$(get_active_wayland_session)" || { log 'No active Wayland session - recovery skipped'; exit 0; } if [[ "${SESSION##*:}" != "$GRAPHICAL_VT" ]]; then log 'Requested VT does not belong to the active Wayland session' exit 1 fi if [[ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then log 'Missing HYPRLAND_INSTANCE_SIGNATURE; import the Hyprland environment into systemd --user' exit 1 fi monitor_present() { timeout 5 hyprctl -j monitors | jq -e --arg name "$MONITOR" 'any(.[]; .name == $name)' >/dev/null 2>&1 } select_dispatch_syntax() { local status provider DISPATCH_SYNTAX=hyprlang # The active config provider, not the version, determines dispatch syntax. # Older Hyprland versions do not expose `status`; their dispatch is legacy. if status="$(timeout 5 hyprctl -j status 2>/dev/null)" && provider="$(jq -er '.configProvider | select(type == "string")' <<< "$status" 2>/dev/null)"; then case "$provider" in lua|hyprlang) DISPATCH_SYNTAX="$provider" ;; *) log "Unsupported Hyprland config provider: $provider"; return 1 ;; esac else log 'Hyprland config provider unavailable; trying legacy dispatch syntax' fi log "Using $DISPATCH_SYNTAX dispatch syntax" } dispatch_checked() { local action="$1" reply status shift reply="$(timeout 5 hyprctl dispatch "$@" 2>&1)" status=$? # Some hyprctl versions return exit 0 even for an IPC error response. if (( status != 0 )) || [[ "$reply" != ok ]]; then log "Hyprland $action failed (exit $status): ${reply:-no response}" return 1 fi } refresh_monitor() { if [[ "$DISPATCH_SYNTAX" == lua ]]; then # MONITOR is restricted to alphanumeric characters, underscores and '-'. dispatch_checked DPMS "hl.dsp.dpms({ action = \"on\", monitor = \"$MONITOR\" })" && dispatch_checked focus "hl.dsp.focus({ monitor = \"$MONITOR\" })" else dispatch_checked DPMS dpms on "$MONITOR" && dispatch_checked focus focusmonitor "$MONITOR" fi } log "Recovery started for $MONITOR on VT $GRAPHICAL_VT" sleep "$HDMI_SETTLE_SECONDS" || exit 1 if ! monitor_present; then log "Hyprland IPC unavailable or configured output $MONITOR absent - recovery aborted" exit 1 fi select_dispatch_syntax || exit 1 if ! refresh_monitor; then log 'Hyprland refresh failed - recovery aborted' exit 1 fi # Recheck after settling/IPC, immediately before the privileged switch. if [[ "$(get_active_wayland_session)" != "$SESSION" ]]; then log 'Active session changed while settling - recovery skipped' exit 0 fi TEMP_VT=1 [[ "$GRAPHICAL_VT" == 1 ]] && TEMP_VT=2 # Check both directions before leaving the desktop (actual sudo can still fail). for vt in "$TEMP_VT" "$GRAPHICAL_VT"; do if ! timeout 5 sudo -n -l /usr/bin/chvt "$vt" >/dev/null 2>&1; then log "sudo permission check failed for VT $vt - recovery aborted" exit 1 fi done if [[ "$(get_active_wayland_session)" != "$SESSION" ]]; then log 'Active session changed during permission checks - recovery skipped' exit 0 fi NEEDS_RETURN=0 cleanup() { local status=$? trap - EXIT HUP INT TERM if (( NEEDS_RETURN )); then log "Attempting emergency return to graphical VT $GRAPHICAL_VT" if ! timeout 5 sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then log "Emergency return failed; manually switch to VT $GRAPHICAL_VT" status=1 fi fi exit "$status" } trap cleanup EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM log "VT refresh: $TEMP_VT -> $GRAPHICAL_VT" # Set before chvt to cover a signal arriving as the command completes. NEEDS_RETURN=1 if ! timeout 5 sudo -n /usr/bin/chvt "$TEMP_VT"; then log "Failed to switch to temporary VT $TEMP_VT" exit 1 fi sleep "$VT_SWITCH_DELAY_SECONDS" || exit 1 if ! timeout 5 sudo -n /usr/bin/chvt "$GRAPHICAL_VT"; then log "Failed to return to graphical VT $GRAPHICAL_VT" exit 1 fi NEEDS_RETURN=0 sleep "$VT_RETURN_SETTLE_SECONDS" || exit 1 if ! refresh_monitor; then log 'Final Hyprland refresh failed' exit 1 fi log "Recovery completed for $MONITOR"