Repository: HikariKnight/ScopeBuddy
Pull request #1: feat: add SCB_AUTO_{RES|HDR|VRR} for automatic kde display property injection
Author: login: porkloin
Created: 2025-03-15T19:49:02Z | Merged: 2025-03-29T18:30:46Z

Apologies for the unsolicited pull request!

I've been maintaining my own bash script for gamescope management for a while, and I'm really excited to junk it now that scopebuddy replaces like 80% of the behavior of my script.

However, there was one major piece of functionality that I had in my gamescope manager that scopebuddy doesn't, which is automatic updating of display properties depending on my active/primary display in KDE.

I move my desktop on the weekends a lot to play games on my TV, which is a 4k display, whereas my display at my desk is a 3440x1440 ultrawide. I got really tired of having to update my gamescope args every time I moved it. Likewise, when I stream games with sunshine, I typically use my HDMI dummy plug, and if I'm streaming to a non-hdr enabled handheld like steam deck I have a `do` cmd bash script that sets the dummy plug display to 1280x800 + hdr off.

Anyway, all of that is to say: I think many users have use cases where they would like the machine running scope buddy to be able to dynamically set values for `-H`, `-W` and conditionally include or remove `--hdr-enabled` or `--enable-vrr` based on their context and not have to screw with their gamescope/scopebuddy config every time they change displays or virtual displays.

I don't know if you really see this type of functionality as something that you want to support in scopebuddy, but I figured I'd open a PR to at least start the conversation! I won't be offended if you decide it's not really a good fit :smile: 

Usage steps are:
- Add `SCB_AUTO_{RES|HDR|VRR}=1` to ~/.config/scopebuddy.conf 
- `scopebuddy -- glxgears`
- Debug echo should show values for `-H`, `-W`, `--adaptive-sync`, `--enable-hdr` that reflect the current KDE primary dispay
- If not KDE, everything should run as usual (no error)

Non-bazzite users will need to install JQ since I'm using it to parse kscreen output.

The most potentially brittle part is probably the sed stuff that tries to do targeted overrides of -H, -W, and -r. If you've got better ideas for how to approach that I'm all ears.

Thanks for all the hard work on scopebuddy!


Diff:
diff --git a/README.md b/README.md
index b1d4061..1daee7a 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@ Features:
 * Enables you to define per-game arguments for gamescope
 * Enables you to define global and per-game ENV variables
 * Optionally use scb just for ENV var management
+* Automatically infer display output args (output width, height, refresh rate, HDR, or VRR flags) from primary display (KDE only)
 
 ## Documentaion and usage
 Look at https://docs.bazzite.gg/Advanced/scopebuddy/
@@ -30,6 +31,10 @@ Look at https://docs.bazzite.gg/Advanced/scopebuddy/
 * [gamescope](https://github.com/ValveSoftware/gamescope)
 * perl
 
+if using `$SCB_AUTO_RES`/`$SCB_AUTO_HDR`/`$SCB_AUTO_VRR`:
+* `jq` (installed by default on Bazzite - other distros may need to install manually)
+* KDE Plasma desktop (Gnome support coming soon)
+
 #### Using curl:
 ```bash
 sudo curl -Lo /usr/local/bin/scopebuddy https://raw.githubusercontent.com/HikariKnight/ScopeBuddy/refs/heads/main/bin/scopebuddy
diff --git a/bin/scopebuddy b/bin/scopebuddy
index 931a12e..79daf23 100755
--- a/bin/scopebuddy
+++ b/bin/scopebuddy
@@ -15,8 +15,12 @@
 #   "~/.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
-SCB_VER="1.0.0"
-echo "Running ScopeBuddy version: $SCB_VER"
+
+##########
+# Globals
+##########
+
+SCB_VER="1.1.0"
 
 gamescope_opts=""
 command=""
@@ -34,6 +38,116 @@ SCB_CONFIGDIR="$HOME/.config/scopebuddy"
 # By default we will never be in gamemode
 SCB_GAMEMODE=0
 
+# 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}
+
+###################
+# Helper functions
+###################
+
+# Appends a substring to $gamescope_apts 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 and logs if any dependency is missing.
+kde_auto_args_preflight() {
+    local missing=""
+    # Check for KDE session
+    if [ -z "$KDE_FULL_SESSION" ]; then
+        missing+="KDE_FULL_SESSION not set; "
+    fi
+    # Check that jq is available
+    if ! command -v jq >/dev/null 2>&1; then
+        missing+="jq not available; "
+    fi
+    # Check that kscreen-doctor is available
+    if ! command -v kscreen-doctor >/dev/null 2>&1; then
+        missing+="kscreen-doctor not available; "
+    fi
+    if [ -n "$missing" ]; then
+        echo "An SCB_AUTO_* argument was passed (auto res, auto hdr, auto vrr) but the following dependencies failed: $missing"
+        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-doctor -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-doctor -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}
+    '
+}
+
+
+#######
+# Main
+#######
+echo "Running ScopeBuddy version: $SCB_VER"
 # 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
@@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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
+    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
@@ -114,6 +256,15 @@ else
 # 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 KDE 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 gamescsope.
+#SCB_AUTO_RES=1
+#SCB_AUTO_HDR=1
+#SCB_AUTO_VRR=1
+# 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
 ###
@@ -139,6 +290,11 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        echo -e "$LOGLINE" > "$SCB_LOGFILE"
+    fi
     eval "env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
 fi


Reviews:
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T13:41:32Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T16:43:21Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T22:54:21Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T23:20:06Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:53:13Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T22:25:33Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T16:21:40Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T16:26:02Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T16:30:57Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-22T17:12:01Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T17:43:33Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T17:45:02Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T22:59:32Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T23:07:08Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T23:09:06Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T23:11:46Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T23:12:23Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T23:13:28Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T23:16:04Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T00:18:44Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T00:20:02Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-29T00:21:15Z
user: login: HikariKnight
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T00:25:26Z
user: login: porkloin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-29T02:55:32Z

Review comments:
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: HikariKnight
body: this replaces/removes whatever the user has set as input resolution, meaning it will default to `1280x720`
example from debug log
```
+ KDE_REFRESH=60
+ '[' -n '-r 60 -w 1280 -h 720' ']'
++ echo '-r 60 -w 1280 -h 720'
++ sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g'
+ gamescope_opts=' -w 1280 -h 720'
+ gamescope_opts='-W 1920 -H 1080 -r 60'
```

```
+ KDE_REFRESH=60
+ '[' -n '-r 60 -w 1280 -h 800' ']'
++ echo '-r 60 -w 1280 -h 800'
++ sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g'
+ gamescope_opts=' -w 1280 -h 800'
+ gamescope_opts='-W 1920 -H 1080 -r 60'
```
created_at: 2025-03-18T13:41:31Z
author_association: MEMBER
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: porkloin
body: Oops, yeah the arg overriding stuff was kind of a mess. It should be fixed now and I did some more extensive testing on my end this time.

I have been talking to someone in the ublue/bazzite discord about how the upcoming Gnome 48 release has a utility very similar to kscreen-doctor built in to it and will probably try to add a GNOME_AUTO_ARGS equivalent when that drops.

All of that is to say, when fixing this I just went ahead and just reworked the arg replacement stuff into a reusable function. That way if I come back to add a GNOME version I don't have a zillion random sed lines that all do more or less the same thing. And maybe there will be other cases where you want to do arbitrary arg overrides/replacements in the future? I stuffed all the helper funcs at the top of the file but underneath everything that looked kinda like global var defs, but feel free to let me know if you want it to go somewhere else.
created_at: 2025-03-18T16:43:20Z
author_association: COLLABORATOR
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: HikariKnight
body: think the bazzite-gnome image ships with `gnome-randr`, ideally we should focus on whats there by default in gnome (but since gnome has nothing atm, we ship gnome-randr in bazzite). but i also do not want to scope creep this project. the auto resolution just seems like a nobrainer to add tbh
created_at: 2025-03-19T22:54:21Z
author_association: MEMBER
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: porkloin
body: Yeah, I don't use gnome on my bazzite install personally, but I installed it on a spare drive to futz around with yesterday and was looking at gnome-randr.  In general, I was noticing that the output of xrandr (and gnome-randr by extension) is kinda not very ergonomic and doesn't support outputting into a serialized or parseable format.  I just hate the idea of adding a bunch of custom xrandr output parsing logic into what otherwise is a pretty nice and tidy bash script for scopebuddy.

That said, the thing that is being released in Gnome 48 (gdctl) has some cool features, but also doesn't support outputting to yaml or json, which sorta stinks. If you ctrl+f on [the code review for gdctl though](https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/4190/diffs#3eedce22ac011e1e3462d0f126789a89871a5c6f) the maintainer was suggesting a `--format=yaml,json` flag.

I spoke with Chrono on the ublue discord about gdctl since they were aware of the project, I guess it's broadly designed as a replacement for gnome-randr and gnome-monitor-config (which is popular community tool for managing mutter dbus, I guess 🤷 ). I kinda feel like if we wanted to mirror this KDE_AUTO_ARGS thing with an equivalent GNOME_AUTO_ARGS, I'd rather use whatever is bundled directly with mutter/gnome project itself.

Anyway, I'd be happy to work on a way to mirror the functionality for GNOME in the future, but I think that might have to wait until after I upstream a JSON formatter into `gdctl` first. Otherwise like I mentioned before, we're gonna have to build some kinda nasty parsers for dealing with xrandr output. `gdctl` is python so I can actually contrib there more easily than some linux projects 🙃

As far as I'm concerned, I don't think I can contribute a gnome version as part of this initial scopebuddy PR unfortunately 😢 
created_at: 2025-03-19T23:20:06Z
author_association: COLLABORATOR
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: HikariKnight
body: oh yeah definitively not in scope for current PR, i should have some time to test this weekend.
Maybe we should rename the arg to `SCB_AUTO_RES` so we wont have an arg for kde and one for gnome :thinking: 
Actually we keep it as KDE now and for migration later when `gdctl` gets a format arg we can just make the KDE env var pass into `SCB_AUTO_RES` if it exist and if it is feasible to have just 1 env var for it later when `gdctl` is fleshed out.

Anyways i will test this in the weekend, had some stuff happen irl i have had to deal with, which has delayed me.
created_at: 2025-03-20T15:53:11Z
author_association: MEMBER
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: porkloin
body: > Maybe we should rename the arg to SCB_AUTO_RES

Good idea! Do you think there's value in splitting it up into dedicated args for resolution, hdr, and vrr?

Something like:
- `SCB_AUTO_RES` (overrides values for `-W {your-primary-display-w-res}` and `-H {your-primary-display-h-res}`
- `SCB_AUTO_HDR` (adds or removes `--hdr-enabled` if primary display has hdr enabled
- `SCB_AUTO_VRR` (adds or removes `--adaptive-sync` if primary display has vrr set to auto or enabled)

Since I split up some of the overriding logic, it should be pretty straightforward to split the behaviors per env variable.

And until we have the ability to support Gnome we can just keep the kde preflight stuff in place and note in the docs that it's a KDE-only feature, I guess? I took a brief look at the `gdctl` source code last night and I think adding json output formatting should be totally doable, but might take quite a bit of time between contrib and getting into an actual fedora release. In the interim we might be able to figure out if there's some way to use gnome-randr instead or some other mechanism 🤷 
created_at: 2025-03-20T22:25:33Z
author_association: COLLABORATOR
diff_hunk: @@ -90,16 +90,95 @@ if [ -f "$SCB_CONFIGFILE" ]; then
         # shellcheck disable=SC1090
         source "$SCB_CONFIGDIR/AppID/${STEAM_APPID}.conf"
     fi
-
+  
     # If the user has supplied ANY args to gamescope, do not load the SCB_GAMESCOPE_ARGS
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # Helper func to fail fast if the user is trying to set KDE_AUTO_ARGS
+    # but doesn't have the right pre-reqs installed or isn't in KDE.
+    kde_auto_args_preflight() {
+        if [ -z "$SCB_KDE_AUTO_ARGS" ]; then
+            return 1
+        fi
+        local missing=""
+        # Check for KDE session
+        if [ -z "$KDE_FULL_SESSION" ]; then
+            missing+="KDE_FULL_SESSION not set; "
+        fi
+        # Check that jq is available
+        if ! command -v jq >/dev/null 2>&1; then
+            missing+="jq not available; "
+        fi
+        # Check that kscreen-doctor is available
+        if ! command -v kscreen-doctor >/dev/null 2>&1; then
+            missing+="kscreen-doctor not available; "
+        fi
+        if [ -n "$missing" ]; then
+            echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
+            return 1
+        fi
+        return 0
+    }
+
+    if kde_auto_args_preflight; then
+        # if multidisplay, sort to get the "primary". KDE settings uses 
+        # a boolean style "primary" toggle, but kscreen uses each display 
+        # with a "priority" int.
+        kde_get_primary_display() {
+            kscreen-doctor -j | jq -c '
+                if (.outputs | length) == 1 then
+                    .outputs[0]
+                else
+                    (.outputs | map(select(.enabled == true)) | sort_by(.priority))[0]
+                end
+            '
+        }
+        kde_get_mode_info() {
+            mode_id=$(kde_get_primary_display | 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}
+            '
+        }
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+
+        # Remove any existing width (-W), height (-H), or refresh (-r) flags from gamescope_opts.
+        # We will replace them with the inferred values from kscreen-doctor in a few lines.
+        if [ -n "$gamescope_opts" ]; then
+            gamescope_opts=$(echo "$gamescope_opts" | sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g')
path: bin/scopebuddy
user: login: HikariKnight
body: Oh yeah once merged we will mention this is a KDE only feature until gnome provides their own thing.
this way we avoid adding arbitrary dependencies (as nice as gnome-randr is), if the DE does not provide a tool to grab info we need then it is not worth supporting if you ask me.

Also yes i see there is value in variables for at least HDR and VRR as both are very temperamental with some monitors.
created_at: 2025-03-22T16:21:40Z
author_association: MEMBER
diff_hunk: @@ -95,11 +193,35 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ -n "$SCB_KDE_AUTO_ARGS" ] && kde_auto_args_preflight; then
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-r[[:space:]]*[0-9]+" "-r $KDE_REFRESH")
path: bin/scopebuddy
user: login: HikariKnight
body: I would not recommend blindly replacing the refresh rate, there are situations for some games you want to limit refresh rate to 60 or some other value due to bad coding in the game (animations tied to framerate, and the game has no fps cap)
created_at: 2025-03-22T16:26:02Z
author_association: MEMBER
diff_hunk: @@ -95,11 +193,35 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ -n "$SCB_KDE_AUTO_ARGS" ] && kde_auto_args_preflight; then
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-r[[:space:]]*[0-9]+" "-r $KDE_REFRESH")
path: bin/scopebuddy
user: login: porkloin
body: Would you be OK with it if I put it behind another flag (`SCB_AUTO_FRAME_LIMIT`?) I'm also updating all of my variable checks to be explicit `$ARG -eq 1` checks isntead of -n or -z, so what I'm imagining would happen here is a user sets their "global" scopebuddy frame limiter to their monitor refresh, and then in per-appid scb conf can unset it/manually override.

The replace_or_append_args bit will only override if the user hasn't specified their own value for refresh/frame limit/`-r`, and if it's also in independently controllable `$SCB_AUTO_FRAME_LIMIT` then they can also revert to setting no frame limit at all, or exempting their global setting from using it.

LMK! I'm actively making some changes right now, so it's convenient that you're reviewing atm :)
created_at: 2025-03-22T16:30:57Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +193,35 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     fi
+
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ -n "$SCB_KDE_AUTO_ARGS" ] && kde_auto_args_preflight; then
+        # hdr and vrr data are stored on the display
+        KDE_HDR_STATE=$(kde_get_primary_display | jq -r '.hdr')
+        KDE_VRR_STATE=$(kde_get_primary_display | jq -r '.vrrPolicy')
+        # width/height/refresh are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info | jq -r '.height')
+        KDE_REFRESH=$(kde_get_mode_info | jq -r '.refresh')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-r[[:space:]]*[0-9]+" "-r $KDE_REFRESH")
path: bin/scopebuddy
user: login: porkloin
body: D'oh, I'm realizing I 100% don't understand what -r is doing, and I see what you mean about explicit setting maybe being undesirable. I'm going to remove it.
created_at: 2025-03-22T17:12:01Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +201,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; then
path: bin/scopebuddy
user: login: HikariKnight
body: `SCB_AUTO_HDR` and `SCB_AUTO_VRR` might not exist and will break the script
you should define them as
`SCB_AUTO_HDR=${SCB_AUTO_HDR:-0}`
in the globals

This will make it default to `0` but lets users set it using an ENV var or by changing the variable in the config files

same goes for `SCB_DEBUG` and `SCB_AUTO_RES`
created_at: 2025-03-28T17:43:33Z
author_association: MEMBER
diff_hunk: @@ -34,6 +38,108 @@ SCB_CONFIGDIR="$HOME/.config/scopebuddy"
 # By default we will never be in gamemode
 SCB_GAMEMODE=0
 
+###################
+# Helper functions
+###################
+
+# Appends a substring to $gamescope_apts 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 $KDE_AUTO_ARGS
+# Returns 0 and logs if any dependency is missing.
+kde_auto_args_preflight() {
+    local missing=""
+    # Check for KDE session
+    if [ -z "$KDE_FULL_SESSION" ]; then
+        missing+="KDE_FULL_SESSION not set; "
+    fi
+    # Check that jq is available
+    if ! command -v jq >/dev/null 2>&1; then
+        missing+="jq not available; "
+    fi
+    # Check that kscreen-doctor is available
+    if ! command -v kscreen-doctor >/dev/null 2>&1; then
+        missing+="kscreen-doctor not available; "
+    fi
+    if [ -n "$missing" ]; then
+        echo "SCB_KDE_AUTO_ARGS defined but not all required conditions are met: $missing"
path: bin/scopebuddy
user: login: HikariKnight
body: old reference to `SCB_KDE_AUTO_ARGS` (also in the comment above the function)
created_at: 2025-03-28T17:45:02Z
author_association: MEMBER
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: HikariKnight
body: ok this is more a question from me since i dont use or notice VRR that much but does `--adaptive-sync` require `-r` to be set in gamescope, as gamescope will default to 60hz otherwise.
If it is a requirement then it would make sense to auto set the hz as long as `SCB_AUTO_VRR` is `1`

then for games where they need to lock it to 60hz they can then just set auto VRR to 0 and set the hz through the gamescope args

other than that i think it looks great from my testing now
created_at: 2025-03-28T22:59:32Z
author_association: MEMBER
diff_hunk: @@ -139,6 +290,20 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        if [ -f "$FILE" ]; then
+            # Count the total number of lines in the file
+            LINE_COUNT=$(wc -l < "$SCB_LOGFILE")
+            # If there are more than 100 lines, retain only the last 100 lines
+            if [ "$LINE_COUNT" -gt 100 ]; then
+              tail -n 100 "$FILE" > "$FILE.tmp"
+              mv "$FILE.tmp" "$FILE"
+            fi
+        fi
+        echo -e "$LOGLINE\n" >> "$SCB_LOGFILE"
+    fi
path: bin/scopebuddy
user: login: HikariKnight
body: I think personally it would make sense to delete the old log first, since we will only see the output of scopebuddy, not the game or steam here
created_at: 2025-03-28T23:07:08Z
author_association: MEMBER
diff_hunk: @@ -139,6 +290,20 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        if [ -f "$FILE" ]; then
+            # Count the total number of lines in the file
+            LINE_COUNT=$(wc -l < "$SCB_LOGFILE")
+            # If there are more than 100 lines, retain only the last 100 lines
+            if [ "$LINE_COUNT" -gt 100 ]; then
+              tail -n 100 "$FILE" > "$FILE.tmp"
+              mv "$FILE.tmp" "$FILE"
+            fi
+        fi
+        echo -e "$LOGLINE\n" >> "$SCB_LOGFILE"
+    fi
path: bin/scopebuddy
user: login: porkloin
body: I think the way this is being done using `tail` will cause the 100 most recent log lines to be persisted because tail's `-n` flag does the last N lines from the bottom of the file, and we're appending with `>>`, so I think we're on the same page here.
created_at: 2025-03-28T23:09:05Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: porkloin
body: I don't think `--adaptive-sync` requires `-r`. I just tested it now and given this debug output from scopebuddy:

```
Launching: env -u LD_PRELOAD gamescope -f --mangoapp -W 3440 -H 1440 --hdr-enabled --adaptive-sync -- env LD_PRELOAD=/usr/lib/extest/libextest.so:/var/home/porkloin/.local/share/Steam/ubuntu12_32/gameoverlayrenderer.so:/var/home/porkloin/.local/share/Steam/ubuntu12_64/gameoverlayrenderer.so  "/var/home/porkloin/.local/share/Steam/ubuntu12_32/steam-launch-wrapper" "--" "/var/home/porkloin/.local/share/Steam/ubuntu12_32/reaper" "SteamLaunch" "AppId=2246340" "--" "/var/home/porkloin/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point" "--verb=waitforexitandrun" "--" "/var/home/porkloin/.local/share/Steam/compatibilitytools.d/GE-Proton9-26/proton" "waitforexitandrun" "/var/home/porkloin/.local/share/Steam/steamapps/common/MonsterHunterWilds/MonsterHunterWilds.exe"
```
I get adaptive sync behavior observable via mangoapp and my monitors OSD that shows the refresh not being pinned to 60 and also matching the in game rate when below native refresh (175hz for my display). Uploading a photo of OSD in a moment!
created_at: 2025-03-28T23:11:46Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: porkloin
body: ![image](https://github.com/user-attachments/assets/f5f52401-ae85-4448-8163-0a7382058bfe)
created_at: 2025-03-28T23:12:22Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: porkloin
body: Slight delay on the tracking from the OSD refresh value so they're not at an identical value when this picture was taken, but as I walked around in game it was tracking with the game-level refresh.
created_at: 2025-03-28T23:13:28Z
author_association: COLLABORATOR
diff_hunk: @@ -139,6 +290,20 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        if [ -f "$FILE" ]; then
+            # Count the total number of lines in the file
+            LINE_COUNT=$(wc -l < "$SCB_LOGFILE")
+            # If there are more than 100 lines, retain only the last 100 lines
+            if [ "$LINE_COUNT" -gt 100 ]; then
+              tail -n 100 "$FILE" > "$FILE.tmp"
+              mv "$FILE.tmp" "$FILE"
+            fi
+        fi
+        echo -e "$LOGLINE\n" >> "$SCB_LOGFILE"
+    fi
path: bin/scopebuddy
user: login: porkloin
body: Oh, reading this again I think I see what you mean - just delete the log file every time? Or delete it and start fresh any time we exceed 100 lines? Either way, this whole thing felt a little unnecessary, but I could see someone eventually getting a really big log file if they left it enabled for years and years :laughing: 
created_at: 2025-03-28T23:16:04Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: HikariKnight
body: huh that's odd because it gets locked to 60fps for me in Like a Dragon: Pirate Yakuza unless i explicitly set `-r 180`, i can just write that off as an anomaly i think, will need to test other games tomorrow when i am free.
created_at: 2025-03-29T00:18:44Z
author_association: MEMBER
diff_hunk: @@ -139,6 +290,20 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        if [ -f "$FILE" ]; then
+            # Count the total number of lines in the file
+            LINE_COUNT=$(wc -l < "$SCB_LOGFILE")
+            # If there are more than 100 lines, retain only the last 100 lines
+            if [ "$LINE_COUNT" -gt 100 ]; then
+              tail -n 100 "$FILE" > "$FILE.tmp"
+              mv "$FILE.tmp" "$FILE"
+            fi
+        fi
+        echo -e "$LOGLINE\n" >> "$SCB_LOGFILE"
+    fi
path: bin/scopebuddy
user: login: HikariKnight
body: start fresh each time, no point keeping previous runs in the log since we only ever care about the current run each time.
created_at: 2025-03-29T00:20:02Z
author_association: MEMBER
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: porkloin
body: Does the game have an explicit framerate or frame cap value in the game settings? For example in FF7R I get 120 max (with variable dips below) even though my screen can do up to 175
created_at: 2025-03-29T00:21:15Z
author_association: COLLABORATOR
diff_hunk: @@ -95,11 +209,39 @@ if [ -f "$SCB_CONFIGFILE" ]; then
     if [ -z "$gamescope_opts" ]; then
         gamescope_opts=$SCB_GAMESCOPE_ARGS
     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')
+    # kde_auto_args_preflight will fail fast if the user doesn't have deps installed.
+    if [ "$SCB_AUTO_RES" -eq 1 ] && kde_auto_args_preflight; then
+        # width/height are mode-specifc values
+        KDE_WIDTH=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.width')
+        KDE_HEIGHT=$(kde_get_mode_info "$PREFER_OUTPUT" | jq -r '.height')
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-W[[:space:]]*[0-9]+" "-W $KDE_WIDTH")
+        gamescope_opts=$(replace_or_append_arg "$gamescope_opts" "-H[[:space:]]*[0-9]+" "-H $KDE_HEIGHT")
+    fi
+    if [ "$SCB_AUTO_HDR" -eq 1 ] && kde_auto_args_preflight; 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
+    fi
+    if [ "$SCB_AUTO_VRR" -eq 1 ] && kde_auto_args_preflight; 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")
path: bin/scopebuddy
user: login: HikariKnight
body: nvm im dumb and had vsync on in game :joy: ignore me
created_at: 2025-03-29T00:25:26Z
author_association: MEMBER
diff_hunk: @@ -139,6 +290,20 @@ else
     LD_PRELOAD_REAL=$LD_PRELOAD
 
     # Unset LD_PRELOAD for gamescope (as it breaks the overlay) then start gamescope with LD_PRELOAD set for %command% instead
-    echo "Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    LOGLINE="Launching: env -u LD_PRELOAD $GAMESCOPE_BIN $gamescope_opts -- env LD_PRELOAD=$LD_PRELOAD_REAL $command"
+    echo "$LOGLINE"
+    if [ "$SCB_DEBUG" -eq 1 ]; then
+        SCB_LOGFILE="$SCB_CONFIGDIR/scopebuddy.log"
+        if [ -f "$FILE" ]; then
+            # Count the total number of lines in the file
+            LINE_COUNT=$(wc -l < "$SCB_LOGFILE")
+            # If there are more than 100 lines, retain only the last 100 lines
+            if [ "$LINE_COUNT" -gt 100 ]; then
+              tail -n 100 "$FILE" > "$FILE.tmp"
+              mv "$FILE.tmp" "$FILE"
+            fi
+        fi
+        echo -e "$LOGLINE\n" >> "$SCB_LOGFILE"
+    fi
path: bin/scopebuddy
user: login: porkloin
body: Done!
created_at: 2025-03-29T02:55:32Z
author_association: COLLABORATOR

PR comments:
user: login: HikariKnight
created_at: 2025-03-17T06:50:36Z
body: This seems to not work properly when you have multiple monitors running

```
+ set -eo pipefail
+ SCB_VER=1.1.0
+ echo 'Running ScopeBuddy version: 1.1.0'
Running ScopeBuddy version: 1.1.0
+ gamescope_opts=
+ command=
+ SCB_NOSCOPE=0
+ GAMESCOPE_BIN=gamescope
+ SCB_CONF=scb.conf
+ SCB_CONFIGDIR=/home/hk/.config/scopebuddy
+ SCB_GAMEMODE=0
+ '[' 0 -eq 1 ']'
+ ps ax
+ grep -P 'steam.sh -.+ -steampal'
+ grep -v grep
+ pgrep -f gamescope-session
+ SCB_CONFIGFILE=/home/hk/.config/scopebuddy/scb.conf
+ [[ 2 -gt 0 ]]
+ '[' -- == -- ']'
+ shift
+ [[ 1 -gt 0 ]]
+ command+=' "vkcube"'
+ shift
+ [[ 0 -gt 0 ]]
+ break
++ echo ' "vkcube"'
++ perl -pe 's/.+"AppId=(\d+)"\s.+/\1/'
+ STEAM_APPID=' "vkcube"'
+ '[' -f /home/hk/.config/scopebuddy/scb.conf ']'
+ source /home/hk/.config/scopebuddy/scb.conf
++ export XKB_DEFAULT_LAYOUT=no
++ XKB_DEFAULT_LAYOUT=no
++ export MANGOHUD_CONFIG=preset=2,toggle_hud=Shift_R+F12,toggle_logging=Shift_R+F2
++ MANGOHUD_CONFIG=preset=2,toggle_hud=Shift_R+F12,toggle_logging=Shift_R+F2
++ export DXVK_HDR=1
++ DXVK_HDR=1
++ export OBS_VKCAPTURE=1
++ OBS_VKCAPTURE=1
++ SCB_GAMESCOPE_ARGS='--mangoapp -f -w 2560 -h 1440 -W 2560 -H 1440 --force-grab-cursor --hdr-enabled -e --adaptive-sync'
+ '[' -f '/home/hk/.config/scopebuddy/AppID/ "vkcube".conf' ']'
+ '[' -z '' ']'
+ gamescope_opts='--mangoapp -f -w 2560 -h 1440 -W 2560 -H 1440 --force-grab-cursor --hdr-enabled -e --adaptive-sync'
+ kde_auto_args_preflight
+ '[' -z 1 ']'
+ local missing=
+ '[' -z true ']'
+ command -v jq
+ command -v kscreen-doctor
+ '[' -n '' ']'
+ return 0
++ kde_get_current_mode_id
++ kde_get_screen_info
++ kscreen-doctor -j
++ jq -r '
                if (.outputs | length) == 1 then
                    .outputs[0].currentModeId
                else
                    .outputs[] | select(.enabled == true and .primary == true) | .currentModeId
                end
            '
+ mode_id=
++ kde_get_mode_info ''
++ local mode_id=
++ kde_get_screen_info
++ kscreen-doctor -j
++ jq -r --arg mode_id '' '
                if (.outputs | length) == 1 then
                    .outputs[0].modes[] | select(.id == $mode_id) | {width: .size.width, height: .size.height, refresh: .refreshRate}
                else
                    .outputs[] | select(.enabled == true and .primary == true) | .modes[] | select(.id == $mode_id) | {width: .size.width, height: .size.height, refresh: .refreshRate}
                end
            '
+ mode_info=
++ kde_get_width ''
++ echo ''
++ jq -r .width
+ KDE_WIDTH=
++ kde_get_height ''
++ echo ''
++ jq -r .height
+ KDE_HEIGHT=
++ kde_get_refresh_rate ''
++ echo ''
++ jq -r .refresh
+ KDE_REFRESH=
++ kde_get_hdr_state
++ kde_get_screen_info
++ kscreen-doctor -j
++ jq -r '.outputs[] | select(.enabled == true) | .hdr'
+ HDR_STATE='true
null
null'
++ kde_get_vrr_state
++ kde_get_screen_info
++ kscreen-doctor -j
++ jq -r '.outputs[] | select(.enabled == true) | .vrrPolicy'
+ VRR_STATE='2
0
0'
+ '[' -n '--mangoapp -f -w 2560 -h 1440 -W 2560 -H 1440 --force-grab-cursor --hdr-enabled -e --adaptive-sync' ']'
++ echo '--mangoapp -f -w 2560 -h 1440 -W 2560 -H 1440 --force-grab-cursor --hdr-enabled -e --adaptive-sync'
++ sed -E 's/ *-W[[:space:]]*[0-9]+//g; s/ *-H[[:space:]]*[0-9]+//g; s/ *-r[[:space:]]*[0-9]+//g'
+ gamescope_opts='--mangoapp -f -w 2560 -h 1440 --force-grab-cursor --hdr-enabled -e --adaptive-sync'
+ gamescope_opts='-W  -H  -r '
+ [[ true
null
null == \t\r\u\e ]]
+ [[ 2
0
0 == 1 ]]
+ [[ 2
0
0 == 2 ]]
+ '[' 0 -eq 1 ']'
+ LD_PRELOAD_REAL=
+ echo 'Launching: env -u LD_PRELOAD gamescope -W  -H  -r  -- env LD_PRELOAD=  "vkcube"'
Launching: env -u LD_PRELOAD gamescope -W  -H  -r  -- env LD_PRELOAD=  "vkcube"
+ eval 'env -u LD_PRELOAD gamescope -W  -H  -r  -- env LD_PRELOAD=  "vkcube"'
++ env -u LD_PRELOAD gamescope -W -H -r -- env LD_PRELOAD= vkcube
```
author_association: MEMBER
user: login: porkloin
created_at: 2025-03-17T19:51:28Z
body: @HikariKnight d'oh, I think my local version must have never actually been hitting the multi-display conditionals because I use kscreen to disable my dummy plug every time I stop streaming on moonlight. It turns out the kscreen key that represents "primary" is actually called "priority" :facepalm: 

I just reworked it and took the opportunity to also just simplify things a bit too. I think it should work correctly for multi-display or single display now.
author_association: COLLABORATOR
user: login: HikariKnight
created_at: 2025-03-18T11:51:21Z
body: Will give it a test later today if i have a moment
author_association: MEMBER
user: login: porkloin
created_at: 2025-03-22T16:58:40Z
body: @HikariKnight sorry to give you a bit of a moving target since I know you just started reviewing again a little bit ago, but I just pushed some additional changes that do the following:

1. Split into 3 separate args (SCB_AUTO_RES, SCB_AUTO_HDR, SCB_AUTO_VRR, ~~SCB_AUTO_FRAME_LIMIT~~ removed this one based on thread above)
2. Check `-eq 1` instead of `-n` to allow users to unset those values via `=0` in subsequent appids
3. Update the helper functions for get primary display and get mode info to respect user flags for -O/--prefer-output, which is a gamescope flag for explicitly determining which display you want to output on. If the user has passed a value for `-O` or `--prefer-output`, the behavior of auto res/hdr/etc. will grab values from that display rather than what is listed as "primary" in KDE/kscreen output. This should theoretically work even if they're only choosing a specific secondary display on a per-appid basis? If they have a global setting for auto res and all games normally open on their "main" display, and then for a specific app_id (your "podcast game") you want it to be on secondary display by default so you can still putz around on the internet on your main display, etc.
author_association: COLLABORATOR
user: login: HikariKnight
created_at: 2025-03-24T17:51:11Z
body: > @HikariKnight sorry to give you a bit of a moving target since I know you just started reviewing again a little bit ago, but I just pushed some additional changes that do the following:
> 
>     1. Split into 3 separate args (SCB_AUTO_RES, SCB_AUTO_HDR, SCB_AUTO_VRR, ~SCB_AUTO_FRAME_LIMIT~ removed this one based on thread above)
> 
>     2. Check `-eq 1` instead of `-n` to allow users to unset those values via `=0` in subsequent appids
> 
>     3. Update the helper functions for get primary display and get mode info to respect user flags for -O/--prefer-output, which is a gamescope flag for explicitly determining which display you want to output on. If the user has passed a value for `-O` or `--prefer-output`, the behavior of auto res/hdr/etc. will grab values from that display rather than what is listed as "primary" in KDE/kscreen output. This should theoretically work even if they're only choosing a specific secondary display on a per-appid basis? If they have a global setting for auto res and all games normally open on their "main" display, and then for a specific app_id (your "podcast game") you want it to be on secondary display by default so you can still putz around on the internet on your main display, etc.

Oh dont worry, i am just letting you cook and i review whenever i find the time :) so do not worry about it being a moving target.

Rather want this in a good finished and coherent state since this is a nice QoL
author_association: MEMBER
user: login: porkloin
created_at: 2025-03-25T14:53:17Z
body: OK, @HikariKnight I think this is ready for review again whenever you have time. I most recently added a variable for generating a debug log at `$HOME/.config/scopebuddy/scopebuddy.log` if the user sets `$SCB_DEBUG`, and otherwise I think your feedback so far has all been integrated.
author_association: COLLABORATOR
user: login: HikariKnight
created_at: 2025-03-28T17:50:05Z
body: sorry for the late review
author_association: MEMBER
user: login: porkloin
created_at: 2025-03-28T18:03:29Z
body: > sorry for the late review

No problem! Just pushed with fixes for all referenced above.
author_association: COLLABORATOR