#!/usr/bin/bash
# Special thanks to @coolavery for the initial script for this
# https://github.com/ValveSoftware/gamescope/issues/835#issuecomment-2496383830
#
# Initial 1.0.0 version was not possible without the help of
# * @coolavery
# * @HikariKnight
# * @Zeglius
# * @tulilirockz
# * @EPOCHvoyager
# * @wolfyreload
#
# Purpose of this script:
# * Allow launching gamescope with a default set of environment variables and gamescope args set by the user
#   "~/.config/scopebuddy/scb.conf" will be created with examples after first run
# * Serve as a temporary workaround for fixing the steam overlay when using nested gamescope on desktop steam until fixed upstream
set -eo pipefail

##########
# Globals
##########
SCB_VER="1.4.0"

gamescope_opts=""
command=""

# Set SCB to use gamescope by default
SCB_NOSCOPE=${SCB_NOSCOPE:-0}

# Set default gamescope binary
GAMESCOPE_BIN=${GAMESCOPE_BIN:-gamescope}

KSCREEN_COMMAND=${KSCREEN_COMMAND:-kscreen-doctor}

# Set default gdctl command for GNOME
GDCTL_COMMAND=${GDCTL_COMMAND:-gdctl}

# Set default gnome-randr command for GNOME (alternative to gdctl)
GNOME_RANDR_COMMAND=${GNOME_RANDR_COMMAND:-gnome-randr}

# Set default wlr-randr command
WLR_RANDR_COMMAND=${WLR_RANDR_COMMAND:-wlr-randr}

# Set default home config directory
XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
# Configure SCB config
SCB_CONF=${SCB_CONF:-"scb.conf"}
SCB_CONFIGDIR="$XDG_CONFIG_HOME/scopebuddy"

# Default off for SCB_APPENDMODE (this is better for testing, but is not 1:1 behavior with gamescope)
# With APPENDMODE enabled, scopebuddy will pass all args and $SCB_GAMESCOPE_ARGS to gamescope with the args to scopebuddy added last.
SCB_APPENDMODE=${SCB_APPENDMODE:-0}

# Set APPID used for current game config
SCB_APPID=${SCB_APPID:-}

# Default off for SCB_DEBUG
SCB_DEBUG=${SCB_DEBUG:-0}

# Default off for SCB_AUTO_*
SCB_AUTO_RES=${SCB_AUTO_RES:-0}
SCB_AUTO_HDR=${SCB_AUTO_HDR:-0}
SCB_AUTO_VRR=${SCB_AUTO_VRR:-0}
SCB_AUTO_REFRESH=${SCB_AUTO_REFRESH:-0}
SCB_AUTO_FRAME_LIMIT=${SCB_AUTO_FRAME_LIMIT:-0}

# We want to ignore -e and --steam gamescope flags as they are currently broken and crash gamescope
SCB_STEAMARGIGNORE=${SCB_STEAMARGIGNORE:-1}

# Add a variable to disable the fix for the steam overlay and steam input in nested gamescope
# See https://xkcd.com/1172/
SCB_NESTEDFIX=${SCB_NESTEDFIX:-1}

############
# Constants
############
# By default we will never be in gamemode
SCB_GAMEMODE=0

# Helper to determine get if any of the SCB_AUTO_* features are enabled
# 0 = none enabled, >0 means at least one is enabled
SCB_ANY_AUTO_FEAT_ENABLED=$(( SCB_AUTO_RES + SCB_AUTO_HDR + SCB_AUTO_VRR + SCB_AUTO_REFRESH + SCB_AUTO_FRAME_LIMIT ))

###################
# Helper functions
###################

# Shows an error dialog/notification to the user and optionally exits
# Usage:
#    show_error_dialog "Title" "Message" [exit_code]
# Params:
#    $1: (Required) Error title
#    $2: (Required) Error message (can include newlines)
#    $3: (Optional) Exit code (if provided, script will exit with this code)
show_error_dialog() {
    local title="$1"
    local message="$2"
    local exit_code="${3:-}"

    # Always print to console
    echo "================================================================================"
    echo "ERROR: $title"
    echo "================================================================================"
    echo "$message"
    echo "================================================================================"

    # Try to show a GUI error dialog using various methods (in order of preference)
    if command -v kdialog >/dev/null 2>&1; then
        # KDE Plasma - native dialog
        kdialog --error "$message" --title "$title" 2>/dev/null &
    elif command -v zenity >/dev/null 2>&1; then
        # GTK-based systems (GNOME, XFCE, etc.)
        zenity --error --text="$message" --title="$title" --width=500 2>/dev/null &
    elif command -v xmessage >/dev/null 2>&1; then
        # Fallback for basic X11 systems
        xmessage -center -buttons "OK:0" "$title: $message" 2>/dev/null &
    elif command -v notify-send >/dev/null 2>&1; then
        # Last resort - notification (less visible but better than nothing)
        notify-send -u critical "$title" "$message" -t 30000 2>/dev/null &
    fi

    # Exit if exit code was provided
    if [ -n "$exit_code" ]; then
        exit "$exit_code"
    fi
}

# Appends a substring to $gamescope_opts if it doesn't already exist
# Usage:
#    gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "<TARGET>")
# Params:
#    $1: (Required) original $gamescope_opts argument string, ex: "-H 1920 -W 1080 --mangoapp" 
#    $2: (Required) target substring. Ex: "--hdr-enabled"). If not found, it will be appended. 
verify_or_append_arg() {
    local argstring="$1"
    local target="$2"
    # append if the value doesn't exist.
    if ! echo "$argstring" | grep -F -q -- "$target"; then
        argstring="$argstring $target"
    fi
    echo "$argstring"
}
# Uses sed to replace a target string if it exists, or append it to the args if it doesn't.
# Usage:
#    gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "<TARGET>" "<REPLACEMENT>")
# Params:
#    $1: (Required) original $gamescope_opts argument string, ex: "-H 1920 -W 1080 --mangoapp" 
#    $2: (Required) target substring. sed extended regex format, ex: "-H[[:space:]]*[0-9]+" (matches strings like "-H 1920") 
#    $3: (Required) replacement substring. Ex: "-H 3440". If $target is not found, this value will be appended.
replace_or_append_arg() {
    local argstring="$1"
    local target="$2"
    local replacement="$3"
    if echo "$argstring" | grep -E -q -- "$target"; then
        # Replace all occurrences of the target with the replacement.
        argstring=$(echo "$argstring" | sed -E "s/${target}/${replacement}/g")
    else
        # Append the replacement value if the target is not found in the original argstring.
        argstring="$argstring $replacement"
    fi
    echo "$argstring"
}
# Validates capability to run $SCB_AUTO_*
# Returns 0 if dependencies are met, 1 otherwise (silent check).
kde_auto_args_preflight() {
    # Check for KDE session
    if [ -z "$KDE_FULL_SESSION" ]; then
        return 1
    fi
    # Check that jq is available
    if ! command -v jq >/dev/null 2>&1; then
        return 1
    fi
    # Check that kscreen-doctor is available
    if ! command -v "$KSCREEN_COMMAND" >/dev/null 2>&1; then
        return 1
    fi
    return 0
}
# Validates capability to run $SCB_AUTO_* on GNOME with gdctl
# Returns 0 if dependencies are met, 1 otherwise (silent check).
gnome_gdctl_auto_args_preflight() {
    # Check for GNOME session
    if [ "$XDG_CURRENT_DESKTOP" != "GNOME" ] && [ "$DESKTOP_SESSION" != "gnome" ]; then
        return 1
    fi
    # Check that jq is available
    if ! command -v jq >/dev/null 2>&1; then
        return 1
    fi
    # Check that gdctl is available and supports --format=json (upstream version check)
    if ! command -v "$GDCTL_COMMAND" >/dev/null 2>&1; then
        return 1
    elif ! "$GDCTL_COMMAND" show --format=json >/dev/null 2>&1; then
        return 1
    fi
    return 0
}
# Validates capability to run $SCB_AUTO_* on GNOME with gnome-randr
# Returns 0 if dependencies are met, 1 otherwise (silent check).
gnome_randr_auto_args_preflight() {
    # Check for GNOME session
    if [ "$XDG_CURRENT_DESKTOP" != "GNOME" ] && [ "$DESKTOP_SESSION" != "gnome" ]; then
        return 1
    fi
    # Check that gnome-randr is available (using GNOME_RANDR_COMMAND which can be customized)
    if ! command -v "$GNOME_RANDR_COMMAND" >/dev/null 2>&1; then
        return 1
    fi
    return 0
}
# Validates capability to run $SCB_AUTO_* on a Wlroots compositor
# Returns 0 if dependencies are met, 1 otherwise (silent check).
wlr_auto_args_preflight() {
    # Check that jq is available
    if ! command -v jq >/dev/null 2>&1; then
        return 1
    fi
    # Check that wlr-randr is available
    if ! command -v "$WLR_RANDR_COMMAND" >/dev/null 2>&1; then
        return 1
    fi
    return 0
}
# Gets the user's primary display via kscreen-doctor.
# Returns a single kscreen JSON object for the display obj.
kde_get_primary_display() {
    # we want to respect user settings for -O or --prefer-output.
    # if so, jsut get that by name.
    local prefer="$1"
    if [ -n "$prefer" ]; then
        # Try to select the output by its exact name
        "$KSCREEN_COMMAND" -j | jq -c --arg prefer "$prefer" '
            .outputs | map(select(.name == $prefer)) | if length > 0 then .[0] else empty end
        '
    else
        # if multidisplay, sort to get the "primary". KDE's settings panel
        # has boolean style "primary" toggle, but kscreen uses a "priority" int value.
        # If there's only one, we just get the first one.
        "$KSCREEN_COMMAND" -j | jq -c '
            if (.outputs | length) == 1 then
                .outputs[0]
            else
                (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
            end
        '
    fi
}
# Selectively extracts mode info from kscreen doctor for use with SCB_AUTO_*.
# More fields are available on the mode, the jq selection can be updated if more
# fields are needed later.
kde_get_mode_info() {
    local prefer="$1"
    # Ensure we're getting the enabled mode for the primary display or the -o/--prefer-output value
    mode_id=$(kde_get_primary_display "$prefer"| jq -r '.currentModeId')
    kde_get_primary_display | jq -r --arg mode_id "$mode_id" '
        .modes[] | select(.id == $mode_id) | {width: .size.width, height: .size.height, refresh: .refreshRate}
    '
}
# Gets the user's primary display via gdctl for GNOME.
# Returns a single JSON object for the display.
gnome_gdctl_get_primary_display() {
    # we want to respect user settings for -O or --prefer-output.
    # if so, just get that by connector name.
    local prefer="$1"
    # shellcheck disable=SC2155
    local gdctl_output=$("$GDCTL_COMMAND" show --format json --current-mode --properties)
    if [ -n "$prefer" ]; then
        # Try to select the output by its connector name
        echo "$gdctl_output" | jq -c --arg prefer "$prefer" '
            .monitors | to_entries[] | select(.value.connector == $prefer) | .value
        '
    else
        # Get the primary display from logical_monitors
        echo "$gdctl_output" | jq -c '
            (.logical_monitors[] | select(.primary == true) | .monitors[0]) as $primary_name |
            .monitors | to_entries[] | select(.key == $primary_name) | .value
        '
    fi
}
# Selectively extracts mode info from gdctl for use with SCB_AUTO_*.
gnome_gdctl_get_mode_info() {
    local prefer="$1"
    gnome_gdctl_get_primary_display "$prefer" | jq -r '
        .current_mode | {width: .resolution.width, height: .resolution.height, refresh: .refresh_rate}
    '
}
# Gets the user's primary display connector via gnome-randr for GNOME.
# Returns the connector name (e.g., "DP-3").
gnome_randr_get_primary_display() {
    # we want to respect user settings for -O or --prefer-output.
    local prefer="$1"
    if [ -n "$prefer" ]; then
        # Return the preferred output if specified
        echo "$prefer"
    else
        # Get the primary display connector from gnome-randr query -s
        "$GNOME_RANDR_COMMAND" query -s | awk '
            /^logical monitor [0-9]+:/ { in_monitor=1; primary=0; next }
            in_monitor && /primary: yes/ { primary=1; next }
            in_monitor && /associated physical monitors:/ { in_physical=1; next }
            in_physical && primary && /^\t[A-Z]/ {
                # Extract the connector name (first field after tab)
                match($0, /^\t([A-Z0-9-]+)/, arr)
                print arr[1]
                exit
            }
            /^logical monitor [0-9]+:/ { in_monitor=1; in_physical=0; primary=0 }
        '
    fi
}
# Selectively extracts mode info from gnome-randr for use with SCB_AUTO_*.
# Returns JSON with width, height, and refresh rate of the active mode.
gnome_randr_get_mode_info() {
    local prefer="$1"
    local connector
    connector=$(gnome_randr_get_primary_display "$prefer" 2>/dev/null)

    if [ -z "$connector" ]; then
        echo "{}"
        return
    fi

    # Query the specific connector and extract the active mode (marked with *)
    "$GNOME_RANDR_COMMAND" query "$connector" | awk -v connector="$connector" '
        # Look for lines with the active mode marked with *
        /[0-9]+x[0-9]+@[0-9.]+/ {
            # Check if this line contains the refresh rate with *
            if (match($0, /([0-9]+)x([0-9]+)@[0-9.]+(\+vrr)?[[:space:]]+[0-9]+x[0-9]+[[:space:]]+([0-9.]+)\*/, arr)) {
                width = arr[1]
                height = arr[2]
                refresh = arr[4]
                printf "{\"width\": %s, \"height\": %s, \"refresh\": %s}\n", width, height, refresh
                exit
            }
        }
    '
}
# Check if VRR is active on the primary display via gnome-randr.
# Returns "true" if VRR is active, "false" otherwise.
gnome_randr_check_vrr() {
    local prefer="$1"
    local connector
    connector=$(gnome_randr_get_primary_display "$prefer" 2>/dev/null)

    if [ -z "$connector" ]; then
        echo "false"
        return
    fi

    # Query the specific connector and check if the active mode has +vrr
    local vrr_result
    vrr_result=$("$GNOME_RANDR_COMMAND" query "$connector" | awk '
        # Look for the active mode line (contains *)
        /[0-9]+x[0-9]+@[0-9.]+/ {
            # Check if this line has * and +vrr
            if ($0 ~ /\*/ && $0 ~ /\+vrr/) {
                print "true"
                found=1
                exit
            }
        }
        END {
            if (!found) print "false"
        }
    ')
    echo "$vrr_result"
}
# Gets the user's primary display via wlr-randr for Wlroots
# Returns a single JSON object for the display.
wlr_get_primary_display() {
    # we want to respect user settings for -O or --prefer-output.
    # if so, just get that by connector name.
    local prefer="$1"
    # shellcheck disable=SC2155
    local gdctl_output=$("$WLR_RANDR_COMMAND" --json)
    if [ -n "$prefer" ]; then
        # Try to select the output by its connector name
        echo "$gdctl_output" | jq -c --arg prefer "$prefer" '
            .[] | select(.name == $prefer)
        '
    else
        show_error_dialog "Wlroots Error" "Primary display not supported!" 1
    fi
}
# Selectively extracts mode info from wlr-randr for use with SCB_AUTO_*.
wlr_get_mode_info() {
    local prefer="$1"
    wlr_get_primary_display "$prefer" | jq -r '
        .modes[] | select(.current == true) | {width: .width, height: .height, refresh: .refresh}
    '
}
# Check for non-steam appid patterns and return an standardized auto value for SCB_APPID
nonsteam_appid_detect() {
    local command="$1"
    local SCB_CONFIGDIR="$2"
    local AUTO_APPID="null"
    local APPID_FOLDER="null"

    # Detect Ubisoft Connect (uplay) games
    if echo "$command" | grep "uplay://" > /dev/null; then
        # Set APPID_FOLDER
        APPID_FOLDER="ubisoft"
        # Set auto appid
        AUTO_APPID="$APPID_FOLDER/"$(echo "$command" | perl -pe 's/.+"uplay:\/\/launch\/(\d+)"\s+/\1/')
    # Detect games launched by Heroic Games Launcher
    elif echo "$command" | grep "heroic://" > /dev/null; then
        # Set APPID_FOLDER
        APPID_FOLDER="heroic"
        # Set auto appid
        AUTO_APPID="$APPID_FOLDER/"$(echo "$command" | perl -pe 's/.+"heroic:\/\/launch\?appName=(.+)&.+\s+/\1/')
    fi

    # Make config folder
    if [ ! -d "$SCB_CONFIGDIR/AppID/$APPID_FOLDER" ] && [ "$APPID_FOLDER" != "null" ]; then
        mkdir -p "$SCB_CONFIGDIR/AppID/$APPID_FOLDER"
    fi

    # Return the detected APPID
    echo "$AUTO_APPID"
}

#######
# Main
#######
echo "Running ScopeBuddy version: $SCB_VER"

# If gamescope is not found, force SCB_NOSCOPE to 1
if ! command -v "$GAMESCOPE_BIN" >/dev/null 2>&1; then
    echo "Setting SCB_NOSCOPE=1 because gamescope was not found"
    SCB_NOSCOPE=1
fi


# If SCB_NOSCOPE is set to 1 and we are not using a custom SCB_CONF
if [ "$SCB_NOSCOPE" -eq 1 ] && [ "$SCB_CONF" == "scb.conf" ]; then
    # Use noscope.conf for default values
    SCB_CONF="noscope.conf"
fi

# If steam is potentially running inside gamescope
# shellcheck disable=SC2009
if ps ax | grep -P "steam.sh -.+ -steampal" | grep -v grep || [ "$XDG_CURRENT_DESKTOP" = "gamescope" ] ; then
    # If steam is potentially running in gamemode
    # Force SCB_NOSCOPE to 1
    SCB_NOSCOPE=1
    # Set SCB_GAMEMODE to 1
    SCB_GAMEMODE=1
    # Use gamemode.conf for default values
    SCB_CONF="gamemode.conf"
fi

# Finalize SCB_CONFIGFILE
SCB_CONFIGFILE="$SCB_CONFIGDIR/$SCB_CONF"

# Split the args at -- into gamescope_opts and command
while [[ $# -gt 0 ]]; do
    if [ "${1:-}" == "--" ]; then
        shift
        # Add remaining args as individually double quoted args (should stop double quoting being a requirement)
        while [[ $# -gt 0 ]]; do
            # Wrap each entry in %command% in quotes to make it easier for scripting
            # Escape the last \ in $1 if the last character is \ otherwise the %command% will not launch
            if [[ "$1" =~ \\$ ]]; then
                # shellcheck disable=SC2089
                command+=" \"$(echo "$1" | sed -E 's/\\$/\\\\/')\""
            else
                # shellcheck disable=SC2089
                command+=" \"$1\""
            fi
            shift
        done
        
        # Exit loop when done
        break
    fi

    # If the steam arg ignore function is enabled, ignore the -e and --steam flag in gamescope
    if [ "$1" == "-e" ] || [ "$1" == "--steam" ] && [ "$SCB_STEAMARGIGNORE" == "1" ]; then
        echo "Ignoring $1 flag for gamescope as it is currently broken"
        shift
    else
        # Add arg to gamescope_opts and go to next loop
        gamescope_opts+=" $1"
        shift
    fi
done

# If $SCB_APPID is not set, attempt to auto detect APPID for Steam
if [ -z "$SCB_APPID" ]; then
    # Get the Steam APPID from %command%
    AUTO_APPID=$(echo "$command" | perl -pe 's/.+"AppId=(\d+)"\s.+/\1/')
    if ! [[ $AUTO_APPID =~ ^[0-9]+$ ]]; then
        AUTO_APPID=$(nonsteam_appid_detect "$command" "$SCB_CONFIGDIR")
    fi
    SCB_APPID="$AUTO_APPID"
fi

# Load the SCB config file if it exists then apply the default args
if [ -f "$SCB_CONFIGFILE" ]; then
    # Source the config from SCB_CONF
    # shellcheck disable=SC1090
    source "$SCB_CONFIGFILE"

    # If a config exists for this games APPID and SCB_CONF is set to scb.conf (default)
    if [ -f "$SCB_CONFIGDIR/AppID/${SCB_APPID}.conf" ]; then
        # Source the APPID specific config, overriding any similar values set by SCB_CONF
        # shellcheck disable=SC1090
        source "$SCB_CONFIGDIR/AppID/${SCB_APPID}.conf"
    fi

    # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
    if [ -z "$gamescope_opts" ] && [ "$SCB_APPENDMODE" == 0 ]; then
        gamescope_opts=$SCB_GAMESCOPE_ARGS
    elif [ "$SCB_APPENDMODE" == 1 ]; then
        gamescope_opts="$SCB_GAMESCOPE_ARGS $gamescope_opts"
    fi

    # Attempt to extract a preferred display from gamescope_opts.
    # This sed command looks for either -O or --prefer-output followed by whitespace and a non‑space value.
    PREFER_OUTPUT=$(echo "$gamescope_opts" | sed -nE 's/.*(-O|--prefer-output)[[:space:]]+([^[:space:]]+).*/\2/p')

    # Determine which desktop environment backend to use for auto-detection.
    # Run KDE/GNOME preflight once and remember the result to avoid repeated messages.
    SCB_DETECT="none"
    if kde_auto_args_preflight; then
        SCB_DETECT="kde"
    elif gnome_gdctl_auto_args_preflight; then
        SCB_DETECT="gnome_gdctl"
    elif gnome_randr_auto_args_preflight; then
        SCB_DETECT="gnome_randr"
    elif wlr_auto_args_preflight; then
        SCB_DETECT="wlroots"
    else
        SCB_DETECT="none"
    fi

    # Warn if auto-detection is requested but no backend was detected
    if [ "$SCB_DETECT" = "none" ] && [ "$SCB_ANY_AUTO_FEAT_ENABLED" -gt 0 ]; then
        # Build list of enabled AUTO flags
        ENABLED_FLAGS=""
        [ "$SCB_AUTO_RES" -eq 1 ] && ENABLED_FLAGS="${ENABLED_FLAGS}SCB_AUTO_RES, "
        [ "$SCB_AUTO_HDR" -eq 1 ] && ENABLED_FLAGS="${ENABLED_FLAGS}SCB_AUTO_HDR, "
        [ "$SCB_AUTO_VRR" -eq 1 ] && ENABLED_FLAGS="${ENABLED_FLAGS}SCB_AUTO_VRR, "
        [ "$SCB_AUTO_REFRESH" -eq 1 ] && ENABLED_FLAGS="${ENABLED_FLAGS}SCB_AUTO_REFRESH, "
        [ "$SCB_AUTO_FRAME_LIMIT" -eq 1 ] && ENABLED_FLAGS="${ENABLED_FLAGS}SCB_AUTO_FRAME_LIMIT, "
        ENABLED_FLAGS="${ENABLED_FLAGS%, }"  # Remove trailing comma and space

        ERROR_MSG="You have the following auto-detection flags enabled: ${ENABLED_FLAGS}

However, your desktop environment doesn't support auto-detection.

Please disable these flags in your ScopeBuddy configuration.

Required dependencies:
  • KDE Plasma: kscreen-doctor and jq
  • GNOME: gdctl (with --format=json) or gnome-randr
  • Wlroots: wlr-randr (with --json)

Current desktop: ${XDG_CURRENT_DESKTOP:-not set}
Session: ${DESKTOP_SESSION:-not set}

For more information, see:
https://github.com/HikariKnight/ScopeBuddy#auto-detection-features-scb_auto_"

        show_error_dialog "ScopeBuddy Auto-Detection Error" "$ERROR_MSG" 1
    fi

    # Use the detected backend for SCB_AUTO_* checks
    if [ "$SCB_AUTO_RES" -eq 1 ]; then
        if [ "$SCB_DETECT" = "kde" ]; then
            # width/height are mode-specific values from KDE
            WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
            HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
        elif [ "$SCB_DETECT" = "gnome_gdctl" ]; then
            # width/height are mode-specific values from GNOME via gdctl
            WIDTH=$(gnome_gdctl_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
            HEIGHT=$(gnome_gdctl_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
        elif [ "$SCB_DETECT" = "gnome_randr" ]; then
            # width/height are mode-specific values from GNOME via gnome-randr
            MODE_INFO=$(gnome_randr_get_mode_info "$PREFER_OUTPUT")
            WIDTH=$(echo "$MODE_INFO" | jq -r '.width')
            HEIGHT=$(echo "$MODE_INFO" | jq -r '.height')
        elif [ "$SCB_DETECT" = "wlroots" ]; then
            # width/height are mode-specific values from the Wlroots compositor via wlr-randr
            MODE_INFO=$(wlr_get_mode_info "$PREFER_OUTPUT")
            WIDTH=$(echo "$MODE_INFO" | jq -r '.width')
            HEIGHT=$(echo "$MODE_INFO" | jq -r '.height')
        else
            WIDTH=""
            HEIGHT=""
        fi
        if [ -n "$WIDTH" ] && [ -n "$HEIGHT" ]; then
            gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $WIDTH")
            gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $HEIGHT")
        fi
    fi
    if [ "$SCB_AUTO_HDR" -eq 1 ]; then
        # Combo var to determine if we should add wayland and wayland hdr
        # variables in NOSCOPE mode.
        if [ "$SCB_NOSCOPE" -eq 1 ] && \
           [ "$SCB_GAMEMODE" -eq 0 ]; then
            AUTO_WAYLAND_HDR_ENABLE=1
        else
            AUTO_WAYLAND_HDR_ENABLE=0
        fi
        if [ "$SCB_DETECT" = "kde" ]; then
            KDE_HDR_STATE=$(kde_get_primary_display "$PREFER_OUTPUT" | jq -r '.hdr')
            if [[ "$KDE_HDR_STATE" == "true" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--hdr-enabled")
            fi
            if [ "$AUTO_WAYLAND_HDR_ENABLE" -eq 1 ]; then
                export PROTON_ENABLE_WAYLAND=1
                export PROTON_ENABLE_HDR=1
            fi
        elif [ "$SCB_DETECT" = "gnome_gdctl" ]; then
            # Check if HDR is enabled on GNOME via gdctl (bt2100 color mode indicates HDR)
            GNOME_GDCTL_HDR_STATE=$(gnome_gdctl_get_primary_display "$PREFER_OUTPUT" | jq -r '."color-mode"')
            if [[ "$GNOME_GDCTL_HDR_STATE" == "bt2100" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--hdr-enabled")
            fi
            if [ "$AUTO_WAYLAND_HDR_ENABLE" -eq 1 ]; then
                export PROTON_ENABLE_WAYLAND=1
                export PROTON_ENABLE_HDR=1
            fi
        elif [ "$SCB_DETECT" = "gnome_randr" ]; then
            # Check if HDR is enabled on GNOME via gnome-randr (color-mode: 1 indicates HDR/bt2100)
            CONNECTOR=$(gnome_randr_get_primary_display "$PREFER_OUTPUT" 2>/dev/null)
            GNOME_RANDR_HDR_STATE=$("$GNOME_RANDR_COMMAND" query "$CONNECTOR" | awk '/^color-mode:/ {print $2}')
            if [[ "$GNOME_RANDR_HDR_STATE" == "1" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--hdr-enabled")
            fi
            if [ "$AUTO_WAYLAND_HDR_ENABLE" -eq 1 ]; then
                export PROTON_ENABLE_WAYLAND=1
                export PROTON_ENABLE_HDR=1
            fi
        elif [ "$SCB_DETECT" = "wlroots" ]; then
            # Check if HDR is enabled on Wlroots
            CONNECTOR=$(wlr_get_primary_display "$PREFER_OUTPUT" 2>/dev/null)
            # TODO: Not supported; stub for future support
            if [ "$AUTO_WAYLAND_HDR_ENABLE" -eq 1 ]; then
                export PROTON_ENABLE_WAYLAND=1
                export PROTON_ENABLE_HDR=1
            fi
        fi
    fi
    if [ "$SCB_AUTO_VRR" -eq 1 ]; then
        if [ "$SCB_DETECT" = "kde" ]; then
            KDE_VRR_STATE=$(kde_get_primary_display "$PREFER_OUTPUT" | jq -r '.vrrPolicy')
            if [[ "$KDE_VRR_STATE" == 1 || "$KDE_VRR_STATE" == 2 ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--adaptive-sync")
            fi
        elif [ "$SCB_DETECT" = "gnome_gdctl" ]; then
            # Check if VRR is enabled on GNOME via gdctl (refresh-rate-mode == "variable")
            GNOME_GDCTL_VRR_STATE=$(gnome_gdctl_get_primary_display "$PREFER_OUTPUT" | jq -r '.current_mode.properties."refresh-rate-mode"')
            if [[ "$GNOME_GDCTL_VRR_STATE" == "variable" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--adaptive-sync")
            fi
        elif [ "$SCB_DETECT" = "gnome_randr" ]; then
            # Check if VRR is enabled on GNOME via gnome-randr (active mode has +vrr)
            GNOME_RANDR_VRR_STATE=$(gnome_randr_check_vrr "$PREFER_OUTPUT")
            if [[ "$GNOME_RANDR_VRR_STATE" == "true" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--adaptive-sync")
            fi
        elif [ "$SCB_DETECT" = "wlroots" ]; then
            # Check if VRR is enabled on Wlroots via wlr-randr
            WLR_VRR_STATE=$(wlr_get_primary_display "$PREFER_OUTPUT" | jq -r '.adaptive_sync')
            if [[ "$WLR_VRR_STATE" == "true" ]]; then
                gamescope_opts=$(verify_or_append_arg "$gamescope_opts" "--adaptive-sync")
            fi
        fi
    fi
    if [ "$SCB_AUTO_REFRESH" -eq 1 ]; then
        if [ "$SCB_DETECT" = "kde" ]; then
            REFRESH_RATE=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "gnome_gdctl" ]; then
            REFRESH_RATE=$(gnome_gdctl_get_mode_info "$PREFER_OUTPUT" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "gnome_randr" ]; then
            MODE_INFO=$(gnome_randr_get_mode_info "$PREFER_OUTPUT")
            REFRESH_RATE=$(echo "$MODE_INFO" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "wlroots" ]; then
            MODE_INFO=$(wlr_get_mode_info "$PREFER_OUTPUT")
            REFRESH_RATE=$(echo "$MODE_INFO" | jq -r '.refresh')
        else
            REFRESH_RATE=""
        fi
        if [ -n "$REFRESH_RATE" ]; then
            gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-r[[:space:]]*[0-9.]+" "-r $REFRESH_RATE")
        fi
    fi
    if [ "$SCB_AUTO_FRAME_LIMIT" -eq 1 ]; then
        if [ "$SCB_DETECT" = "kde" ]; then
            REFRESH_RATE=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "gnome_gdctl" ]; then
            REFRESH_RATE=$(gnome_gdctl_get_mode_info "$PREFER_OUTPUT" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "gnome_randr" ]; then
            MODE_INFO=$(gnome_randr_get_mode_info "$PREFER_OUTPUT")
            REFRESH_RATE=$(echo "$MODE_INFO" | jq -r '.refresh')
        elif [ "$SCB_DETECT" = "wlroots" ]; then
            MODE_INFO=$(wlr_get_mode_info "$PREFER_OUTPUT")
            REFRESH_RATE=$(echo "$MODE_INFO" | jq -r '.refresh')
        else
            REFRESH_RATE=""
        fi
        if [ -n "$REFRESH_RATE" ]; then
                gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "--framerate-limit[[:space:]]*[0-9.]+" "--framerate-limit $REFRESH_RATE")
        fi
    fi
else
    # Make the default config file
    if [ ! -d "$SCB_CONFIGDIR/AppID" ]; then
        mkdir -p "$SCB_CONFIGDIR/AppID"
    fi
    # TODO: this content is functionally impossible to update for existing users, so it's maybe
    # not a great place to stuff README-style content. Once the file is created they'll never get new
    # versions on subsequent scopebuddy releases.
    cat << 'EOF' > "$SCB_CONFIGFILE"
# This is the config file that let's you assign defaults for gamescope when using the scopebuddy script
# lines starting with # are ignored
# Conf files matching the games Steam AppID stored in ~/.conf/scopebuddy/AppID/ will be sourced after
# ~/.config/scopebuddy/scb.conf or whichever file you specify with SCB_CONF=someotherfile.conf env var in the launch options.
# 
# Example for always exporting specific environment variables for gamescope
#export XKB_DEFAULT_LAYOUT=no
#export MANGOHUD_CONFIG=preset=2
#
# Example for providing default gamescope arguments through scopebuddy if no arguments are given to the scopebuddy script, this does not need to be exported.
# To not use this default set of arguments, just launch scb with SCB_NOSCOPE=1 or just add any gamescope argument before the '-- %command%' then this variable will be ignored
#SCB_GAMESCOPE_ARGS="--mangoapp -f -w 2560 -h 1440 -W 2560 -H 1440 -r 180 --force-grab-cursor --hdr-enabled -e"
#
# To auto-detect display width, height, refresh, VRR and HDR states, you can use SCB_AUTO_* {RES|HDR|VRR}
# These vars will override any previously set values for -W and -H or append --hdr-enabled and --adaptive-sync
# automatically depending on the current settings for your active display, or the display chosen with -O /
# --prefer-output flags in gamescope. This works on both KDE (via kscreen-doctor) and GNOME (via gdctl).
#SCB_AUTO_RES=1
#SCB_AUTO_HDR=1
#SCB_AUTO_VRR=1
# AUTO_HDR can also be applied without gamescope when running in NOSCOPE mode, however, this requires a Proton
# version with wayland HDR support. This will automatically add PROTON_ENABLE_WAYLAND and PROTON_ENABLE_HDR to env.
# Note that this WILL cause your game to run in wayland mode, which will break the steam overlay.
# An additional variable is required to enable this behavior (SCB_AUTO_HDR must also be set to 1):
#SCB_NOSCOPE_AUTO_HDR=1
# For GNOME users: you can use either gdctl or gnome-randr for auto-detection
# gdctl requires upstream version with --format=json support (will be tested automatically)
# gnome-randr will be used as fallback if gdctl test fails
# To specify a custom gdctl build (must support 'gdctl show --format=json'):
#export GDCTL_COMMAND="$HOME/.local/bin/gdctl-mr4708"
# To debug scopebuddy output, uncomment the following line. After launching games, the executed cmd will be output to ~/.config/scopebuddy/scopebuddy.log
#SCB_DEBUG=1
###
## FOR ADVANCED USE INSIDE AN APPID CONFIG
###
# The config files are treated as a bash script by scopebuddy, this means you can use bash to do simple tasks before the game runs
# or you can check which mode scopebuddy is running in and apply settings accordingly, below are some handy variables for scripting.
# $SCB_NOSCOPE will be set to 1 if we are running in no gamescope mode
# $SCB_GAMEMODE will be set to 1 if we are running inside steam gamemode (which means SCB_NOSCOPE will also be set to 1 due to nested gamescope not working in gamemode)
# $command will contain everything steam expanded %command% into
EOF
fi


pre_command() {
  if [[ -n "$SCB_PRE_COMMAND" ]]; then
    echo "Executing pre-command: $SCB_PRE_COMMAND"
    eval "$SCB_PRE_COMMAND" || echo "Warning: SCB_PRE_COMMAND failed!"
  fi
}

post_command() {
  if [[ -n "$SCB_POST_COMMAND" ]]; then
    echo "Executing post-command: $SCB_POST_COMMAND"
    eval "$SCB_POST_COMMAND"
  fi
}

# Bind post_command to exit signal
trap post_command EXIT

# If SCB_NOSCOPE is set to 1
if [ "$SCB_NOSCOPE" -eq 1 ]; then
    # If we are potentially in gamemode
    if [ "$SCB_GAMEMODE" -eq 1 ]; then
        # Force MANGOHUD=0
        export MANGOHUD=0
    fi

    # Write logfile if SCB_DEBUG is enabled
    LOGLINE="Launching: $(echo "$command" | sed -E 's/\\" /\\\\" /g')"
    echo "$LOGLINE"
    if [ "$SCB_DEBUG" -eq 1 ]; then
        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
        echo -e "$LOGLINE" > "$SCB_LOGFILE"
    fi

    # Launch %command% without gamescope
    eval "$command"
else
    # Set LD_PRELOAD_REAL to empty
    LD_PRELOAD_REAL=""

    # Apply nested gamescope fix for steam overlay and steam input
    if [ "$SCB_NESTEDFIX" -eq 1 ]; then
        # Transfer LD_PRELOAD before we unset it
        LD_PRELOAD_REAL=$LD_PRELOAD
    fi

    # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=\"$LD_PRELOAD_REAL\" $(echo "$command" | sed -E 's/\\" /\\\\" /g')"
    echo "$LOGLINE"
    if [ "$SCB_DEBUG" -eq 1 ]; then
        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
        echo -e "$LOGLINE" > "$SCB_LOGFILE"
    fi
    # run pre_command before gamescope gets exec'd. will proceed even if pre_command fails.
    pre_command
    eval "env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=\"$LD_PRELOAD_REAL\" $command"
fi
