From 52c883b780fc1b473410079a11ca1b321a160994 Mon Sep 17 00:00:00 2001 From: 28allday Date: Mon, 18 May 2026 19:12:42 +0100 Subject: [PATCH] =?UTF-8?q?v0.1.11=20=E2=80=94=20Multi-monitor=20handling:?= =?UTF-8?q?=20disable=20an=20auxiliary=20monitor=20before=20Gaming=20Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 7 +++++ bin/deckshift-settings | 61 +++++++++++++++++++++++++++++++++++++----- deckshift.sh | 24 ++++++++++++++++- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 541bbdf..f160ce4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ Lineage: forked from [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/ ## What's New +### v0.1.11 — Multi-monitor handling: disable an auxiliary monitor before Gaming Mode + +- New env var `OUTPUT_CONNECTOR_TO_DISABLE` (single connector or comma list). When set, `switch-to-gaming` runs `hyprctl keyword monitor ,disable` for each listed connector *before* SDDM restart, while Hyprland is still alive. The disable is runtime-only — when the user returns from Gaming Mode, the new Hyprland reads its static config fresh and the monitor comes back automatically. +- Settings TUI exposes this as a **"Hide monitor"** option in the main menu and on the state panel. The picker lists every connected monitor *except* the gaming one, plus a "(clear)" entry to remove the override. +- Fixes a reported issue on multi-monitor setups (e.g. Framework Desktop + LG DualUp + Gigabyte M27Q) where gamescope would either land on the wrong screen or refuse to start when both monitors were attached. The previous workaround was to physically unplug the second monitor. +- Also fixes a latent bug from v0.1.10: the config-file path in the TUI was supposed to render with `~` instead of `/home/` to fit the panel, but bash's tilde-expansion on the replacement side of `${var/#pat/~}` re-expanded `~` back to `$HOME`, making the substitution a no-op. The replacement is now escaped as `\~`. + ### v0.1.10 — Settings TUI layout polish - Banner, state panel, menu header, and menu items now share a single centred panel column rather than each block centring itself independently. The TUI feels visibly aligned in a Walker floating window of any width — no more drifting elements off to the left while the menu floats to the right. diff --git a/bin/deckshift-settings b/bin/deckshift-settings index 628f11d..9ff32ac 100755 --- a/bin/deckshift-settings +++ b/bin/deckshift-settings @@ -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 <} Hz GPU mode : ${gpu_mode} + Hide monitor : ${disable_connector:-} 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 diff --git a/deckshift.sh b/deckshift.sh index df26de8..01daa09 100755 --- a/deckshift.sh +++ b/deckshift.sh @@ -34,7 +34,7 @@ set -Euo pipefail # -u: Treat unset variables as errors (catches typos in variable names) # -o pipefail: A pipeline fails if ANY command in it fails, not just the last one -DECKSHIFT_VERSION="0.1.10" +DECKSHIFT_VERSION="0.1.11" # Resolve the directory this script lives in so we can find sibling files like # bin/deckshift-settings and applications/deckshift-settings.desktop when @@ -2219,6 +2219,28 @@ notify-send -u normal -t 2000 "Gaming Mode" "Switching to Gaming Mode..." 2>/dev pkill -9 gamescope 2>/dev/null || true pkill -9 -f gamescope-session 2>/dev/null || true sleep 1 + +# Multi-monitor handling — gamescope-session-plus picks an output by env, but +# with two monitors connected it sometimes lands on the wrong one (or refuses +# to start). If OUTPUT_CONNECTOR_TO_DISABLE is set in the user's env conf, +# disable those connectors via hyprctl while Hyprland is still alive so +# gamescope only sees the gaming display. The disable is runtime-only (no +# config edit) so when the user returns from Gaming Mode the new Hyprland +# reads its static config fresh and the monitor comes back automatically. +ENV_CONF="$HOME/.config/environment.d/gamescope-session-plus.conf" +if [[ -f "$ENV_CONF" ]]; then + TO_DISABLE=$(awk -F= '$1=="OUTPUT_CONNECTOR_TO_DISABLE" { sub(/^[^=]*=/,""); v=$0 } END { print v }' "$ENV_CONF") + if [[ -n "$TO_DISABLE" ]]; then + IFS=',' read -ra DISABLE_LIST <<< "$TO_DISABLE" + for conn in "${DISABLE_LIST[@]}"; do + conn="${conn// /}" + [[ -z "$conn" ]] && continue + hyprctl keyword monitor "${conn},disable" 2>/dev/null || true + done + sleep 0.5 + fi +fi + sudo -n chvt 2 2>/dev/null || true sleep 0.3 sudo -n systemctl restart sddm