The real bug behind every "60 Hz stuck in Gaming Mode" report:
- DeckShift installs `gamescope` from Arch's `extra` repo (upstream
Valve binary). That binary does NOT have `--custom-refresh-rates`.
The flag is a ChimeraOS-fork (`gamescope-plus`) addition that
never landed upstream, and the fork is not packaged in AUR for
64-bit Arch.
- The AUR `gamescope-session-git` script (OpenGamingCollective,
ex-ChimeraOS) was written assuming gamescope-plus. It feature-
detects via `gamescope_has_option "--custom-refresh-rates"` and
silently drops the value when missing.
- Net effect: CUSTOM_REFRESH_RATES from the env conf reaches the
session script but never reaches the gamescope binary. Every
refresh-rate selection in the DeckShift TUI since the project
began has been a paper fix; gamescope just launched at the
EDID-preferred mode (usually 60 Hz).
- v0.1.8's comma-list "fix" was correct on paper but the value
never reached the binary, so it didn't actually do anything on
Omarchy. The Framework Desktop user wasn't reporting a hardware-
specific bug — they were the first user to notice a bug that
affected everyone, because their 170 Hz monitor made the regression
obvious. On a 165 Hz Acer Nitro you'd just never notice 60 vs 165
in Steam BPM.
Fix: ./deckshift.sh now patches the installed gamescope-session-plus
in place, adding an elif fallback that uses `--nested-refresh` (a flag
present in every gamescope version) with the highest value from the
CUSTOM_REFRESH_RATES list as the launch rate. Marked with a
DECKSHIFT-NESTED-REFRESH-FALLBACK sentinel comment for idempotency,
re-applied on every install so AUR upgrades that clobber the file
don't silently regress refresh-rate handling.
Implementation: new patch_gamescope_session_plus() function in
deckshift.sh, called from setup_session_switching() after the AUR
install completes. Uses Python regex with a function-based replacement
(re.sub with a string-form replacement was the first attempt and got
caught by Python's backslash processing turning `\n` in the `tr`
argument into a real newline — function callbacks bypass that, so the
literal `\n` reaches the shell as intended).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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.
- Adaptive panel width: min(COLS − 6, 60), floored at 40.
- Terminal-width detection: `stty size </dev/tty` first (kernel-reported,
always reflects the live window) with `tput cols`/80 fallback. Fixes
off-centre rendering in freshly-spawned floating terminals whose terminfo
hasn't caught up to the compositor's actual size yet.
- Config-file path renders as `~/...` instead of `/home/<user>/...` so it
fits the panel.
- Unset resolution shows `<auto>` instead of `?x?` (matches the other unset
placeholders).
Internally: `pad_block` replaces `center_block`'s widest-line-centres-block
behaviour with a single shared left margin; `cmenu` left-aligns the menu
at the panel edge so the `> ` cursor sits in the same column as the state
panel's section headers above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-v0.1.8 users have a scalar CUSTOM_REFRESH_RATES=<rate> in their
gamescope-session-plus.conf that triggers the 60 Hz launch bug. v0.1.8
only fixed the TUI write path, so existing users had to re-open the TUI
and re-pick the rate to migrate. Installer now detects the legacy
scalar format and rewrites to the comma list (60,<rate>) on re-run,
then imports the new value into the running systemd user environment.
Idempotent — already-migrated configs are left alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported by clutchmuffin: TUI saved e.g. CUSTOM_REFRESH_RATES=165 but
Gaming Mode always launched at 60 Hz. Two stacked bugs:
1. Settings TUI wrote ~/.config/environment.d/gamescope-session-plus.conf
but never reloaded systemd's user env, so the running user manager
(and therefore gamescope-session-plus@.service) still had the old
values until next login. Now flush_pending calls
`systemctl --user import-environment` / `unset-environment` for the
keys it just touched.
2. CUSTOM_REFRESH_RATES was written as a single scalar. Gamescope's
--custom-refresh-rates is a list of *switchable* rates, not a
launch-rate selector — and with no safe 60 Hz fallback in the list,
some DRM/NVIDIA paths drop to the EDID-preferred 60 Hz on first
launch. TUI now writes a comma list (e.g. 60,165). show_state and
confirm_risky_save de-list to the highest member for display /
validation.
Adds a troubleshooting README entry documenting the Steam BPM
client-side rate persistence as the canonical first-launch workaround.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump banner version 0.1.3 → 0.1.6.
- Drop the "moving toward distro-portability" / "cross-distro is the
next direction" notes — DeckShift is Omarchy-only.
- Add What's New entries for v0.1.4 (portal-recovery race fix),
v0.1.5 (clipboard via Walker restart), v0.1.6 (Omarchy-only cleanup).
- Combine the screen-sharing and clipboard symptoms into a single
troubleshooting block (same root cause) and replace the old racy
manual-restart command with `touch /tmp/.deckshift-just-returned
&& /usr/local/bin/deckshift-portal-recovery`, with a note explaining
why the marker is required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chromium/Firefox "Share desktop" / "Share window" silently broke after
the SDDM-restart cycle: xdg-desktop-portal-hyprland stays bound to the
killed Hyprland instance, so the new compositor's screencasts get no
frames. Tab sharing kept working because Chromium captures tabs
internally, bypassing the portal.
switch-to-desktop now drops /tmp/.deckshift-just-returned before the
SDDM restart. A new helper (/usr/local/bin/deckshift-portal-recovery),
exec-once'd from ~/.config/hypr/autostart.conf, checks for that marker
on every Hyprland start and bounces the portal + pipewire user services
when present. No marker = no-op.
README troubleshooting updated with both this and the suspend-access
fix from the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump header to v0.1.3
- Restructured "What's New" with v0.1.3, v0.1.2, and v0.1.1/v0.1.0 sections
- Settings TUI table now lists both [hybrid-nvidia] and [hybrid-amd] options
- New top-level "Recovery from a Black Screen" section with TTY-fallback
commands (loginctl terminate-user, systemctl restart sddm)
- "Performance Mode" section explains the saved-state save/restore and the
Omarchy + AC caveat (omarchy-powerprofiles-init re-applies performance on
every Hyprland session start when plugged in)
- New "Hybrid laptop note" under NVIDIA-Specific Notes explaining why
direct-NVIDIA mode black-screens on eDP-1 and pointing to [hybrid-nvidia]
- Configuration section clarifies the installer no longer writes display
keys — those are owned exclusively by the Settings TUI
- Updated "What Gets Installed" with gum/jq deps and saved-state file
- Updated Uninstalling section to cover ~/.cache/deckshift, polkit reload,
etc.
Steam Deck-style gaming mode for Linux + Hyprland. Lineage:
Super-Shift-S-Omarchy-Deck-Mode → Omarchy Deck → DeckShift (renamed for
distro-portability — distro-agnostic features on the roadmap).
Currently targets Omarchy (Arch + Hyprland + SDDM + iwd). Includes:
- Press Super+Shift+S to enter Gaming Mode (Gamescope + Steam Big
Picture), Super+Shift+R to return to desktop
- NVIDIA GSP-aware driver branch selection (legacy nvidia-580xx-utils
for Maxwell/Pascal/Volta cards)
- AMD support (vulkan-radeon, libvdpau)
- Intel support (vulkan-intel, intel-media-driver) with generation-aware
performance warning before continuing
- Idempotent package installs via Omarchy's omarchy-pkg-add
- Optional Xbox Bluetooth controller support (xpadneo-dkms)
- Settings TUI launched from Walker (deckshift-settings) for adjusting
monitor / GPU / resolution / refresh rate after install:
* Buffered changes — explicit Save and exit / Cancel
* hyprctl-based mode detection with monitor-capability filtering
* Save-time warning if values exceed monitor capability
* Esc returns to main menu without crashing
- Multilib check removed (Omarchy ships with multilib enabled)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>