v0.1.11 — Multi-monitor handling: disable an auxiliary monitor before Gaming Mode

Reported on a Framework Desktop (AMD AI MAX 380) + Gigabyte M27Q +
LG DualUp setup: with both monitors attached, gamescope would either
land on the wrong screen or refuse to start, and writing
OUTPUT_CONNECTOR=DP-X alone wasn't enough to fix it. The workaround
shipped by the user was to manually patch /usr/share/gamescope-session-plus
to disable the other monitor before launching gamescope.

DeckShift now handles this natively, without touching the
gamescope-session-plus script (which is ChimeraOS's, not ours):

- New env var OUTPUT_CONNECTOR_TO_DISABLE (single connector or
  comma list) written to ~/.config/environment.d/gamescope-session-plus.conf
  alongside the other display keys.

- switch-to-gaming reads it and runs `hyprctl keyword monitor X,disable`
  for each listed connector BEFORE the SDDM restart, while Hyprland is
  still alive (hyprctl needs a live IPC socket). The disable is
  runtime-only — Hyprland's static config isn't touched — so when the
  user returns from Gaming Mode the new Hyprland reads its config fresh
  and the monitor comes back automatically. No re-enable step needed.

- Settings TUI exposes this as a "Hide monitor" main-menu item. The
  picker lists every connected monitor EXCEPT the gaming one (so a
  user can't accidentally pick the same connector they just set as
  OUTPUT_CONNECTOR) and includes a "(clear)" entry.

Also fixes a latent bug in v0.1.10's config-path shortening:
${CONF/#$HOME/~} was supposed to render the conf path with ~ but
bash applies tilde-expansion to the replacement side, re-expanding ~
to $HOME and making the substitution a no-op. Escaped as \~ now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday
2026-05-18 19:12:42 +01:00
co-authored by Claude Opus 4.7
parent aaa2f3d768
commit 52c883b780
3 changed files with 84 additions and 8 deletions
+54 -7
View File
@@ -307,7 +307,7 @@ list_gpus() {
# ------------------------------------------------------------------------------
show_state() {
local connector width height refresh vk_adapter dri_prime prime_offload mesa_vk_select
local connector width height refresh vk_adapter dri_prime prime_offload mesa_vk_select disable_connector
connector=$(effective OUTPUT_CONNECTOR)
width=$(effective SCREEN_WIDTH)
height=$(effective SCREEN_HEIGHT)
@@ -322,6 +322,7 @@ show_state() {
dri_prime=$(effective DRI_PRIME)
prime_offload=$(effective __NV_PRIME_RENDER_OFFLOAD)
mesa_vk_select=$(effective MESA_VK_DEVICE_SELECT)
disable_connector=$(effective OUTPUT_CONNECTOR_TO_DISABLE)
# Single GPU-mode line — shows the active mode rather than half-empty rows,
# since the modes are mutually exclusive. AMD hybrid is identified by both
@@ -354,7 +355,7 @@ show_state() {
fi
# Replace $HOME with ~ so the config path fits the panel column.
local conf_display="${CONF/#$HOME/~}"
local conf_display="${CONF/#$HOME/\~}"
cat <<EOF
Gaming Mode display settings${pending_label}
@@ -363,6 +364,7 @@ Gaming Mode display settings${pending_label}
Resolution : ${resolution_label}
Refresh rate : ${refresh:-<auto>} Hz
GPU mode : ${gpu_mode}
Hide monitor : ${disable_connector:-<none>}
Config file : ${conf_display}
EOF
@@ -396,6 +398,49 @@ choose_monitor() {
fi
}
# choose_monitor_to_disable — picks a connector to physically disable (via
# `hyprctl keyword monitor X,disable`) before Gaming Mode launches.
#
# Why this exists: with multiple monitors connected, gamescope-session-plus
# can't reliably target a single output via OUTPUT_CONNECTOR alone — on some
# setups it picks the wrong screen, on others it fails to start at all
# (reported on a Framework Desktop + LG DualUp + Gigabyte M27Q setup).
# Disabling the auxiliary monitor right before SDDM restart guarantees
# gamescope only sees the gaming display.
#
# The disable is runtime-only (hyprctl keyword, not a config edit), so when
# the user returns from Gaming Mode the fresh Hyprland reads its static
# config and the monitor comes back automatically — no re-enable step needed.
choose_monitor_to_disable() {
local choice gaming_monitor
gaming_monitor=$(effective OUTPUT_CONNECTOR)
mapfile -t connected < <(list_connected_monitors)
local -a labels=()
local entry conn
for entry in "${connected[@]}"; do
conn="${entry%%|*}"
# Exclude the gaming monitor — disabling it would be self-defeating.
[[ "$conn" == "$gaming_monitor" ]] && continue
labels+=("$entry")
done
labels+=("(clear / don't hide any monitor)")
if (( ${#labels[@]} == 1 )); then
gum confirm "Only the gaming monitor is connected. Set a connector to disable manually?" || return 0
local connector
connector=$(ginput --prompt "Connector to disable (e.g. HDMI-A-1): ")
[[ -z "$connector" ]] && return 0
pending_set OUTPUT_CONNECTOR_TO_DISABLE "$connector"
return 0
fi
choice=$(cmenu "Select monitor to disable while gaming" "${labels[@]}")
[[ -z "$choice" ]] && return 0
if [[ "$choice" == "(clear"* ]]; then
pending_unset OUTPUT_CONNECTOR_TO_DISABLE
else
pending_set OUTPUT_CONNECTOR_TO_DISABLE "${choice%%|*}"
fi
}
choose_resolution() {
refresh_monitor_data
local choice w h
@@ -699,13 +744,15 @@ main() {
"Resolution" \
"Refresh rate" \
"GPU" \
"Hide monitor" \
"$save_label" \
"$cancel_label")
case "$action" in
"Monitor") choose_monitor ;;
"Resolution") choose_resolution ;;
"Refresh rate") choose_refresh_rate ;;
"GPU") choose_gpu ;;
"Monitor") choose_monitor ;;
"Resolution") choose_resolution ;;
"Refresh rate") choose_refresh_rate ;;
"GPU") choose_gpu ;;
"Hide monitor") choose_monitor_to_disable ;;
"Save and exit"*)
if ! confirm_risky_save; then
continue
@@ -713,7 +760,7 @@ main() {
flush_pending
clear
echo ""
gum style --foreground 212 "Settings saved to ${CONF/#$HOME/~}" | pad_block
gum style --foreground 212 "Settings saved to ${CONF/#$HOME/\~}" | pad_block
gum style --foreground 244 "Changes apply next time you enter Gaming Mode (Super+Shift+S)." | pad_block
sleep 1
return 0