Compare commits

...

7 commits

Author SHA1 Message Date
d7251fe2f3 Add maintainer notes (Omarchy 4 facts, conventions, test checklist)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 21:49:21 +01:00
8ba6e72e17 v0.1.15 — Omarchy 4 (Quickshell + Lua config) compatibility
Omarchy 4's Lua config provider ignores every ~/.config/hypr/*.conf file,
which silently broke DeckShift in two places:

- Keybind: installer wrote only to bindings.conf, so fresh Omarchy 4
  installs never got Super+Shift+S. Now writes hl.unbind + o.bind to
  bindings.lua when present (the default claims the combo and duplicate
  binds both fire), with the bindings.conf path kept as a pre-4 fallback.
- Portal recovery: the exec-once in autostart.conf never ran, disabling
  the v0.1.4 screen-share fix. Now wired via o.launch_on_start in
  autostart.lua, conf fallback kept.

Also:
- --verify checks the Lua files (it previously false-passed against the
  dead bindings.conf) and gains a portal-recovery autostart check.
- Removed Walker/elephant integration: omarchy-restart-walker and
  elephant no longer exist; omarchy-shell owns the clipboard and starts
  fresh each session, so the stale-clipboard restart is unnecessary.
- README: v0.1.15 changelog, app-menu wording, uninstall section now
  covers portal-recovery, the autostart lines, and the pacman cap hook.

v0.1.14 was a reverted, unreleased keybind change; version skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 21:47:14 +01:00
835a939a81 Revert "v0.1.14 — change Gaming Mode launch keybind from Super+Shift+S to Super+Shift+G"
This reverts commit 0844f02a8c.
2026-06-08 21:21:41 +01:00
0844f02a8c v0.1.14 — change Gaming Mode launch keybind from Super+Shift+S to Super+Shift+G
Super+Shift+S collided with other uses; move the enter-Gaming-Mode bind to
Super+Shift+G. Updates the Hyprland bindd line, all comments/messages, and the
README usage references. Adds in-place migration so re-running the installer
rewrites an existing Super+Shift+S bind to Super+Shift+G (the idempotency check
previously skipped because the switch-to-gaming line already existed). Also
syncs the README version header (was 0.1.9) to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:17:26 +01:00
d45f81be36 v0.1.13 — pacman hook keeps gamescope cap_sys_nice across upgrades
File capabilities live on the inode as a security.capability xattr, so
pacman drops cap_sys_nice every time it replaces /usr/bin/gamescope on
upgrade. Performance mode silently regresses (worse frame pacing / input
latency) with no error surfaced.

Installer now drops /usr/share/libalpm/hooks/deckshift-gamescope-cap.hook,
which re-applies cap_sys_nice=eip PostTransaction whenever gamescope is
installed or upgraded. Idempotent; prompted once at install time alongside
the existing cap_sys_nice grant prompt.
2026-05-19 18:03:32 +01:00
a3cb6f28e4 v0.1.12 — Patch gamescope-session-plus so refresh-rate selection actually reaches gamescope
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>
2026-05-18 19:38:33 +01:00
52c883b780 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>
2026-05-18 19:12:42 +01:00
4 changed files with 442 additions and 103 deletions

85
NOTES.md Normal file
View file

@ -0,0 +1,85 @@
# DeckShift — maintainer notes
Working notes for future development sessions. User-facing docs live in README.md.
## Current state (2026-07-27)
- **v0.1.15 released** — commit `8ba6e72`, tagged, pushed to both remotes
(Forgejo `nosignal/deckshift` + GitHub `28allday/deckshift`; the `forgejo`
remote pushes to both).
- v0.1.14 was **skipped**: that label was used by a keybind-change commit
(`0844f02`, Super+Shift+S → Super+Shift+G) that was reverted before release.
- Dev/reference machine: Omarchy 4 desktop (AMD RX 9060 XT), install verified
with `./deckshift.sh --verify` — all checks passed, including the two checks
added in v0.1.15.
## Omarchy 4 facts that shape this codebase
Discovered during the 2026-07-27 compatibility audit; verify against a live
system before assuming they still hold.
1. **Hyprland runs on Omarchy's Lua config provider**`hyprctl systeminfo`
reports `configProvider: lua`. Every `~/.config/hypr/*.conf` file
(bindings.conf, autostart.conf, windows.conf, ...) is **completely ignored**,
even though `hyprland.conf` still contains `source =` lines for them.
Proof method: a bind present only in bindings.conf does not appear in
`hyprctl binds -j`.
2. **User override files are Lua**: `~/.config/hypr/bindings.lua`,
`autostart.lua`, etc., loaded after Omarchy's defaults. API (defined in
`~/.local/share/omarchy/default/hypr/helpers.lua`):
- `o.bind("SUPER + SHIFT + S", "Description", "command")`
- `hl.unbind("SUPER + SHIFT + S")` — required before rebinding a combo the
Omarchy defaults claim; duplicate Hyprland binds BOTH fire.
- `o.launch_on_start("command")` — exec-once equivalent (wraps the command
with `o.launch()`, i.e. uwsm-app).
3. **Walker/elephant are gone**: `omarchy-restart-walker` and `elephant` no
longer exist (`walker` binary may linger as an orphan). The Quickshell-based
omarchy-shell owns the app menu, notifications, and clipboard. The clipboard
holder starts fresh with each Hyprland session, so the pre-4 stale-socket
clipboard bug (v0.1.5/v0.1.6 fix) cannot recur — that code was removed.
`omarchy-restart-shell` exists if a shell bounce is ever needed.
4. **Still present and safe to depend on**: `omarchy-pkg-add`,
`omarchy-hw-nvidia-gsp` / `-without-gsp`, `omarchy-install-gaming-steam`,
`uwsm-app`, the `TUI.float` floating-window rule
(`default/hypr/apps/system.lua`), `xdg-terminal-exec`.
## Conventions
- **Omarchy-only, not distro-portable** — depend on `omarchy-*` helpers freely;
no non-Omarchy fallbacks. (Pre/post-Omarchy-4 *version* branching is fine and
used for the bindings/autostart writers.)
- Lua-first, `.conf` fallback: every Hyprland config write site checks for the
`.lua` override file first, falls back to the legacy `.conf`, warns if
neither exists. Keep new write sites consistent with this.
- All config writes are idempotent (grep-before-append). Re-running
`./deckshift.sh` on an existing install migrates `.conf` wiring to `.lua`
automatically (the stale `.conf` lines are left in place — they're dead
files under Omarchy 4).
- Release flow: bump `DECKSHIFT_VERSION`, README header + changelog entry,
commit `vX.Y.Z — summary`, annotated tag, `git push forgejo master &&
git push forgejo vX.Y.Z` (covers both remotes).
## Testing checklist for future changes
- `bash -n deckshift.sh bin/deckshift-settings` + `shellcheck -S warning` on
both (pre-existing SC2155/SC1090 warnings are known noise).
- `./deckshift.sh --verify` on the dev box.
- After touching keybind/autostart wiring: `hyprctl reload` +
`hyprctl configerrors` must come back clean, and confirm via
`hyprctl binds -j` that the bind is actually live (don't trust file contents
— that's exactly how the v0.1.15 bug hid).
- Full round-trip (enter + return + screen-share + clipboard) when touching
anything in switch-to-gaming / gaming-session-switch / portal recovery.
## Open items / ideas
- Real hardware round-trip test of v0.1.15 portal recovery (enter Gaming Mode,
return, confirm "Share desktop" works in Chromium) — wiring is verified, the
end-to-end path hasn't been re-run since the change.
- SDDM `Relogin=true` has no backoff: a crash-looping gamescope session
respawns several times per second until gamescope-session-plus's 5-strike
short-session tracker recovers. Escape hatch from a tty:
`sudo /usr/local/bin/gaming-session-switch desktop && sudo systemctl restart sddm`.
Worth a proper fix someday.
- The `--noninteractive` mode sketched for NoSignal OS integration was never
committed here; re-implement from scratch if needed.

View file

@ -1,15 +1,47 @@
# DeckShift
**Version 0.1.9** — Steam Deck-style gaming mode for [Omarchy](https://omarchy.com). Press `Super+Shift+S` to enter Gaming Mode (Steam Big Picture in Gamescope), `Super+Shift+R` to return to your desktop.
**Version 0.1.15** — Steam Deck-style gaming mode for [Omarchy](https://omarchy.com). Press `Super+Shift+S` to enter Gaming Mode (Steam Big Picture in Gamescope), `Super+Shift+R` to return to your desktop.
Lineage: forked from [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/nosignal/Super-Shift-S-Omarchy-Deck-Mode), briefly renamed Omarchy Deck, then renamed DeckShift.
> **Target:** [Omarchy](https://omarchy.com) — Arch + Hyprland + SDDM + Walker. DeckShift depends on Omarchy-specific helpers (`omarchy-pkg-add`, `omarchy-restart-walker`, etc.) and is not intended to be cross-distro.
> **Target:** [Omarchy](https://omarchy.com) — Arch + Hyprland + SDDM. DeckShift depends on Omarchy-specific helpers (`omarchy-pkg-add`, `omarchy-install-gaming-steam`, etc.) and is not intended to be cross-distro. Omarchy 4 (Quickshell / Lua config) is the primary target; pre-4 installs are still handled via the legacy `.conf` fallbacks.
[![DeckShift demo](https://img.youtube.com/vi/nj4pLh3spCs/maxresdefault.jpg)](https://youtu.be/nj4pLh3spCs)
## What's New
### v0.1.15 — Omarchy 4 (Quickshell + Lua config) compatibility
Omarchy 4 moved Hyprland onto a Lua config provider (`hyprctl systeminfo` reports `configProvider: lua`) and replaced the Waybar/Walker/Mako desktop stack with the Quickshell-based omarchy-shell. Two things this broke, both fixed:
- **The `Super+Shift+S` keybind never activated on fresh Omarchy 4 installs.** The `*.conf` files under `~/.config/hypr` (including `bindings.conf`, where DeckShift wrote its keybind) are no longer read at all. The installer now writes to `~/.config/hypr/bindings.lua` when it exists — `hl.unbind("SUPER + SHIFT + S")` first (Omarchy's defaults claim that combo, and duplicate Hyprland binds both fire), then `o.bind(...)`. `bindings.conf` remains as a fallback for pre-4 installs.
- **`deckshift-portal-recovery` silently stopped running after returning from Gaming Mode** — its `exec-once` lived in the now-ignored `autostart.conf`, so the screen-share/portal fix from v0.1.4 was effectively disabled. The installer now wires it via `o.launch_on_start(...)` in `~/.config/hypr/autostart.lua`, with the `autostart.conf` fallback kept for pre-4.
- `--verify` now checks the Lua files on Omarchy 4 (previously it false-passed against the dead `bindings.conf`) and gained a portal-recovery autostart check.
- Removed the Walker/elephant integration: `omarchy-restart-walker` and `elephant` no longer exist on Omarchy 4. The elephant `launch_prefix` config step, the Walker refresh after installing the settings launcher, and the clipboard-restart tail of `deckshift-portal-recovery` are all gone. Omarchy 4's shell owns the clipboard and starts fresh with each session, so the stale-clipboard bug those steps fixed can no longer occur. (If you're on pre-4 Omarchy and rely on the clipboard fix, stay on v0.1.13.)
Re-running `./deckshift.sh` on an existing install migrates the keybind and autostart wiring to the Lua files automatically.
*(v0.1.14 was an unreleased keybind change that was reverted; the version number is skipped.)*
### v0.1.13 — Pacman hook keeps gamescope's cap_sys_nice across upgrades
- Linux file capabilities live as an xattr (`security.capability`) on the inode, so every time pacman replaces `/usr/bin/gamescope` during an upgrade the previously-granted `cap_sys_nice=eip` is silently lost. Performance mode keeps "working" but the compositor thread loses its priority boost — worse frame pacing and input latency, with no error surfaced anywhere.
- DeckShift now installs `/usr/share/libalpm/hooks/deckshift-gamescope-cap.hook`, a pacman hook that re-applies `cap_sys_nice=eip` PostTransaction whenever `gamescope` is installed or upgraded. The installer prompts for it the same time it asks for the initial capability grant; if you already consented on a prior install, re-running `./deckshift.sh` adds the hook silently.
- The hook is treated as optional in the verification step, so users who declined performance mode (or declined the cap prompt) won't see a missing-file warning.
### v0.1.12 — Refresh-rate selection actually reaches gamescope now
- **The real bug:** Omarchy installs `gamescope` from Arch's `extra` repo (upstream Valve binary), but the AUR `gamescope-session-git` script (OpenGamingCollective / ex-ChimeraOS fork) was written assuming the ChimeraOS-fork `gamescope-plus` binary that ships `--custom-refresh-rates`. The fork isn't packaged for 64-bit Arch — we can't install it cleanly. The session script feature-detects via `gamescope_has_option "--custom-refresh-rates"`, finds it absent, and **silently drops the `CUSTOM_REFRESH_RATES` value before it reaches gamescope**. Net effect: every refresh-rate selection in the DeckShift TUI since the project began has been a no-op. Gaming Mode has been launching at the EDID-preferred mode (usually 60 Hz) regardless of what the user picked. v0.1.8's "60 Hz fix" was correct on paper but never actually reached the binary on Omarchy.
- **The fix:** `./deckshift.sh` now patches `/usr/share/gamescope-session-plus/gamescope-session-plus` in place, adding an `elif` branch that falls back to `--nested-refresh` (a flag present in every gamescope version) with the highest value from the `CUSTOM_REFRESH_RATES` list as the launch rate. The patch is marked with a `DECKSHIFT-NESTED-REFRESH-FALLBACK` sentinel comment for idempotency, and is re-applied on every install so AUR upgrades that clobber the file don't silently regress refresh-rate handling.
- **What you should do after upgrading:** re-run `./deckshift.sh` once. Future-you, if you ever see Gaming Mode stuck at 60 Hz after a `pacman -Syu` that touched `gamescope-session-git`, just re-run the installer — the patch reapplies cleanly.
### 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 <conn>,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/<user>` 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.
@ -75,7 +107,7 @@ Lineage: forked from [Super-Shift-S-Omarchy-Deck-Mode](https://git.no-signal.uk/
## Settings TUI
After install, launch `DeckShift Settings` from Walker (or run `deckshift-settings` directly) to change Gaming Mode display settings without editing config files:
After install, launch `DeckShift Settings` from the app menu (`Super+Space`) — or run `deckshift-settings` directly — to change Gaming Mode display settings without editing config files:
| Option | What it sets in `gamescope-session-plus.conf` |
|---|---|
@ -123,7 +155,7 @@ chmod +x deckshift.sh
The installer is fully interactive and walks you through each step.
After install, open `DeckShift Settings` from Walker and pick:
After install, open `DeckShift Settings` from the app menu (`Super+Space`) and pick:
- A monitor
- A resolution / refresh rate
@ -138,7 +170,7 @@ Save, then `Super+Shift+S` to launch Gaming Mode.
| Enter Gaming Mode | `Super + Shift + S` |
| Return to Desktop | `Super + Shift + R` *(global keybind monitor catches it inside Gamescope)* |
| Return to Desktop (alternative) | Steam → Power → **Switch to Desktop** |
| Open settings | Walker`DeckShift Settings`, or run `deckshift-settings` |
| Open settings | App menu (`Super+Space`)`DeckShift Settings`, or run `deckshift-settings` |
### Command-Line Options
@ -262,14 +294,14 @@ Package installs use Omarchy's `omarchy-pkg-add` (idempotent, double-checks pacm
| Path | Purpose |
|---|---|
| `~/.config/environment.d/gamescope-session-plus.conf` | Gamescope session config (display + GPU keys) — managed via the Settings TUI |
| `~/.config/hypr/bindings.conf` | Hyprland keybind for `Super+Shift+S` (appended) |
| `~/.config/hypr/bindings.lua` | Hyprland keybind for `Super+Shift+S` (appended; `bindings.conf` on pre-Omarchy-4) |
| `~/.cache/deckshift/saved-state` | Pre-Gaming-Mode CPU governor + power profile (created on entry, cleaned up on exit) |
#### Settings TUI
| Path | Purpose |
|---|---|
| `/usr/local/bin/deckshift-settings` | Gum-based TUI for Gaming Mode display + GPU settings |
| `/usr/share/applications/deckshift-settings.desktop` | Walker launcher (floats via Omarchy's `TUI.float` windowrule) |
| `/usr/share/applications/deckshift-settings.desktop` | App menu launcher (floats via Omarchy's `TUI.float` windowrule) |
## How It Works
@ -469,11 +501,11 @@ See [Recovery from a Black Screen](#recovery-from-a-black-screen) for how to get
- Check PipeWire config exists: `cat /etc/pipewire/pipewire.conf.d/10-gaming-latency.conf`
- Try lower quantum: edit the config and set `default.clock.min-quantum = 128`
**Screen sharing in Chromium / Firefox is broken after returning from Gaming Mode (only "Share a tab" works) — and/or clipboard is dead**
**Screen sharing in Chromium / Firefox is broken after returning from Gaming Mode (only "Share a tab" works)**
Both symptoms have the same root cause: `xdg-desktop-portal-hyprland` and Walker's `elephant.service` (the clipboard listener) are still bound to the killed Hyprland instance after the SDDM restart. Tab capture in Chromium works because it bypasses the portal entirely. Clipboard listening, screen capture, and window capture all go through services that need to be reattached to the live compositor.
Root cause: `xdg-desktop-portal-hyprland` (and the pipewire stack behind it) is still bound to the killed Hyprland instance after the SDDM restart. Tab capture in Chromium works because it bypasses the portal entirely; screen and window capture go through services that need to be reattached to the live compositor. (On pre-4 Omarchy the same stale-socket problem also killed the clipboard via Walker's `elephant.service`; Omarchy 4's shell starts fresh each session, so the clipboard is unaffected there.)
DeckShift handles this automatically via `/usr/local/bin/deckshift-portal-recovery` (autostarted from `~/.config/hypr/autostart.conf`). If you installed before this fix, re-run `./deckshift.sh` to install the helper, or run the recovery manually:
DeckShift handles this automatically via `/usr/local/bin/deckshift-portal-recovery`, autostarted from `~/.config/hypr/autostart.lua` (Omarchy 4) or `autostart.conf` (pre-4). **If you installed before v0.1.15 and are on Omarchy 4, re-run `./deckshift.sh`** — the old `autostart.conf` wiring is ignored by Omarchy 4's Lua config provider, so the helper never ran. You can also run the recovery manually:
```bash
touch /tmp/.deckshift-just-returned && /usr/local/bin/deckshift-portal-recovery
@ -535,7 +567,7 @@ sudo pkill -f steam-library-mount
# Remove scripts
sudo rm -f /usr/local/bin/{switch-to-gaming,switch-to-desktop,gamescope-session-nm-wrapper,\
gaming-session-switch,gaming-keybind-monitor,gamescope-nm-start,gamescope-nm-stop,\
steam-library-mount,deckshift-settings}
steam-library-mount,deckshift-settings,deckshift-portal-recovery}
sudo rm -f /usr/lib/os-session-select
sudo rm -rf /usr/local/lib/gamescope-nvidia
@ -551,6 +583,7 @@ sudo rm -f /etc/polkit-1/rules.d/50-gamescope-networkmanager.rules
sudo rm -f /etc/polkit-1/rules.d/50-udisks-gaming.rules
sudo rm -f /etc/udev/rules.d/99-gaming-performance.rules
sudo rm -f /etc/security/limits.d/99-gaming-memlock.conf
sudo rm -f /usr/share/libalpm/hooks/deckshift-gamescope-cap.hook
# Remove configs
sudo rm -f /etc/sddm.conf.d/zz-gaming-session.conf
@ -565,8 +598,13 @@ rm -f ~/.config/environment.d/gamescope-session-plus.conf
rm -rf ~/.cache/deckshift
sudo rm -f /usr/share/applications/deckshift-settings.desktop
# Strip the Hyprland keybind line
# Strip the Hyprland keybind + portal-recovery autostart lines
# Omarchy 4 (Lua config):
sed -i '/switch-to-gaming/d; /SUPER + SHIFT + S/d' ~/.config/hypr/bindings.lua
sed -i '/deckshift-portal-recovery/d' ~/.config/hypr/autostart.lua
# Pre-4 Omarchy (.conf config):
sed -i '/switch-to-gaming/d' ~/.config/hypr/bindings.conf
sed -i '/deckshift-portal-recovery/d' ~/.config/hypr/autostart.conf
# Reload polkit/udev
sudo systemctl restart polkit

View file

@ -2,7 +2,7 @@
# ==============================================================================
# deckshift-settings — Gaming Mode settings TUI
#
# Launched from Walker (Super+Space → "DeckShift Settings"). Lets the user
# Launched from the app menu (Super+Space → "DeckShift Settings"). Lets the user
# adjust which monitor, GPU, resolution, and refresh rate Gaming Mode uses,
# without editing ~/.config/environment.d/gamescope-session-plus.conf by hand.
#
@ -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

View file

@ -3,13 +3,18 @@
# DeckShift - Steam Deck Mode for Linux + Hyprland
#
# Forked from Super-Shift-S-Omarchy-Deck-Mode (v12.27) → renamed Omarchy Deck →
# renamed DeckShift. Targets Omarchy (Arch + Hyprland + SDDM + Walker). Extended
# with:
# renamed DeckShift. Targets Omarchy (Arch + Hyprland + SDDM). Extended with:
# - NVIDIA GSP-aware driver branch selection (legacy 580xx for Pascal/Maxwell)
# - omarchy-pkg-add idempotent package installs
# - Optional Xbox Bluetooth controller support (xpadneo-dkms)
# - Intel GPU support (Iris Xe / Arc / iGPU)
# - Settings TUI launched from Walker (deckshift-settings)
# - Settings TUI launched from the app menu (deckshift-settings)
#
# Omarchy 4 note: Hyprland now runs on Omarchy's Lua config provider — the
# *.conf files under ~/.config/hypr (bindings.conf, autostart.conf, ...) are
# NOT read any more. Keybind and autostart wiring below prefers the .lua
# override files when present and falls back to the legacy .conf files on
# pre-4 installs.
#
# This script transforms an Omarchy (Arch Linux + Hyprland) desktop into a
# dual-mode system: Desktop Mode (Hyprland) and Gaming Mode (Steam Big Picture
@ -34,7 +39,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.15"
# Resolve the directory this script lives in so we can find sibling files like
# bin/deckshift-settings and applications/deckshift-settings.desktop when
@ -1086,46 +1091,6 @@ EOF
fi
}
# Configures the Elephant app launcher (used in Omarchy) to launch desktop
# applications through uwsm-app. UWSM (Universal Wayland Session Manager)
# ensures apps are properly associated with the Wayland session, which
# prevents issues with apps losing track of their display server.
configure_elephant_launcher() {
local cfg="$HOME/.config/elephant/desktopapplications.toml"
if [[ ! -f "$cfg" ]]; then
return 0
fi
if ! command -v uwsm-app >/dev/null 2>&1; then
return 0
fi
if grep -q '^launch_prefix[[:space:]]*=[[:space:]]*"uwsm-app --"' "$cfg" 2>/dev/null; then
return 0
fi
if grep -q '^launch_prefix[[:space:]]*=' "$cfg" 2>/dev/null; then
sed -i 's|^launch_prefix[[:space:]]*=.*|launch_prefix = "uwsm-app --"|' "$cfg"
else
echo 'launch_prefix = "uwsm-app --"' >> "$cfg"
fi
info "Configured Elephant desktopapplications launch_prefix (uwsm-app)"
restart_elephant_walker
}
# Restarts the Elephant/Walker launcher service so config changes take
# effect immediately without requiring a logout.
restart_elephant_walker() {
if ! systemctl --user show-environment >/dev/null 2>&1; then
return 0
fi
if command -v omarchy-restart-walker >/dev/null 2>&1; then
omarchy-restart-walker >/dev/null 2>&1 || true
return 0
fi
systemctl --user restart elephant.service >/dev/null 2>&1 || true
systemctl --user restart app-walker@autostart.service >/dev/null 2>&1 || true
}
# Installs the core packages that the Gaming Mode scripts themselves need
# (as opposed to Steam's dependencies which are handled separately).
# These include:
@ -1159,27 +1124,66 @@ setup_requirements() {
setup_performance_permissions
setup_fcitx_silence
setup_shader_cache
configure_elephant_launcher
if [[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && command -v gamescope >/dev/null 2>&1; then
if ! getcap "$(command -v gamescope)" 2>/dev/null | grep -q 'cap_sys_nice'; then
local hook_path="/usr/share/libalpm/hooks/deckshift-gamescope-cap.hook"
local needs_cap=false
getcap "$(command -v gamescope)" 2>/dev/null | grep -q 'cap_sys_nice' || needs_cap=true
if $needs_cap || ! sudo test -f "$hook_path"; then
echo ""
echo "================================================================"
echo " GAMESCOPE CAPABILITY REQUEST"
echo "================================================================"
echo ""
echo " Performance mode requires granting cap_sys_nice to gamescope."
echo " Performance mode needs cap_sys_nice on gamescope (better frame"
echo " pacing + lower input latency). Pacman strips file capabilities"
echo " on every gamescope upgrade, so DeckShift also installs a pacman"
echo " hook that re-applies the cap automatically post-upgrade."
echo ""
read -p "Grant cap_sys_nice to gamescope? [Y/n]: " -n 1 -r
read -p "Grant cap_sys_nice + install pacman hook? [Y/n]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
sudo setcap 'cap_sys_nice=eip' "$(command -v gamescope)" || warn "Failed to set capability"
info "Capability granted to gamescope"
if $needs_cap; then
sudo setcap 'cap_sys_nice=eip' "$(command -v gamescope)" || warn "Failed to set capability"
info "Capability granted to gamescope"
fi
install_gamescope_cap_hook
fi
else
install_gamescope_cap_hook
fi
fi
}
# Pacman strips file capabilities (security.capability xattr) every time it
# replaces the gamescope binary on upgrade. Without cap_sys_nice the
# compositor thread loses its priority boost and performance mode silently
# regresses (no error surfaced). This hook re-applies the cap PostTransaction
# whenever gamescope is installed or upgraded. Idempotent — safe to re-run.
install_gamescope_cap_hook() {
local hook_path="/usr/share/libalpm/hooks/deckshift-gamescope-cap.hook"
sudo install -d -m 755 /usr/share/libalpm/hooks
sudo tee "$hook_path" > /dev/null << 'HOOK'
# Managed by DeckShift — DO NOT EDIT.
# Re-applies cap_sys_nice to gamescope after every pacman upgrade. File
# capabilities live on the inode as a security.capability xattr and are lost
# when pacman replaces the binary.
[Trigger]
Type = Path
Operation = Install
Operation = Upgrade
Target = usr/bin/gamescope
[Action]
Description = DeckShift: re-applying cap_sys_nice to gamescope
When = PostTransaction
Exec = /usr/bin/setcap cap_sys_nice=eip /usr/bin/gamescope
HOOK
sudo chmod 644 "$hook_path"
info "Installed pacman hook: $hook_path"
}
# Optional: install Bluetooth Xbox controller support (xpadneo).
# Wired Xbox controllers work without this via the kernel's xpad driver.
# xpadneo-dkms gives proper button mapping and rumble for wireless controllers
@ -1229,11 +1233,11 @@ setup_xbox_controllers() {
info "Pair controllers with Super+Ctrl+B (Omarchy Bluetooth menu)"
}
# Install the Gaming Mode settings TUI and its Walker launcher.
# Install the Gaming Mode settings TUI and its app menu launcher.
# The TUI lets users pick monitor / GPU / resolution / refresh rate after
# install without hand-editing ~/.config/environment.d/gamescope-session-plus.conf.
# The .desktop file uses Omarchy's TUI.float pattern so it pops up as a floating
# terminal window from Walker (Super+Space → "DeckShift Settings").
# terminal window from the app menu (Super+Space → "DeckShift Settings").
setup_settings_tui() {
echo ""
echo "================================================================"
@ -1257,21 +1261,14 @@ setup_settings_tui() {
info "Installing settings TUI to $tui_dst"
sudo install -m 0755 "$tui_src" "$tui_dst" || die "Failed to install settings TUI"
info "Installing Walker launcher to $desktop_dst"
info "Installing app launcher entry to $desktop_dst"
sudo install -m 0644 "$desktop_src" "$desktop_dst" || die "Failed to install desktop entry"
if command -v update-desktop-database >/dev/null 2>&1; then
sudo update-desktop-database /usr/share/applications 2>/dev/null || true
fi
# Refresh Walker so the new entry shows up immediately. omarchy-restart-walker
# handles the elephant.service + walker autostart restart for us; without
# this, the entry only appears after the next Walker restart / login.
if command -v omarchy-restart-walker >/dev/null 2>&1; then
omarchy-restart-walker 2>/dev/null || true
fi
info "Settings TUI installed — launch from Walker (Super+Space → 'DeckShift Settings')"
info "Settings TUI installed — launch from the app menu (Super+Space → 'DeckShift Settings')"
}
# ==============================================================================
@ -1302,6 +1299,95 @@ setup_settings_tui() {
# It also installs ChimeraOS's gamescope-session packages from AUR, which
# provide the base session framework that the Steam Deck uses.
# ==============================================================================
# Patch the installed gamescope-session-plus script to add a --nested-refresh
# fallback for CUSTOM_REFRESH_RATES.
#
# Why this exists:
# - 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.
# - The OpenGamingCollective (ex-ChimeraOS) `gamescope-session-plus` script
# we install from AUR was written assuming gamescope-plus. It feature-
# detects via `gamescope_has_option "--custom-refresh-rates"` and silently
# drops the value when the flag is missing. Result: CUSTOM_REFRESH_RATES
# reaches the script but never reaches gamescope, and Gaming Mode launches
# at the EDID-preferred rate (usually 60 Hz) regardless of TUI selection.
# - This patch adds an `elif` branch that falls back to `--nested-refresh`
# (the older flag that exists in every gamescope version) with the highest
# rate from the comma list as the launch rate.
#
# Idempotent: the patched line carries a `DECKSHIFT-NESTED-REFRESH-FALLBACK`
# marker so re-runs detect "already patched" and skip. Re-applied on every
# ./deckshift.sh run so pacman/AUR upgrades that clobber the script don't
# silently regress refresh-rate handling.
patch_gamescope_session_plus() {
local gsp="/usr/share/gamescope-session-plus/gamescope-session-plus"
if [[ ! -f "$gsp" ]]; then
warn "$gsp not found — skipping refresh-rate fallback patch"
return 0
fi
if grep -q "DECKSHIFT-NESTED-REFRESH-FALLBACK" "$gsp" 2>/dev/null; then
info "gamescope-session-plus already has DeckShift refresh-rate fallback"
return 0
fi
info "Patching gamescope-session-plus to add --nested-refresh fallback..."
local tmp
tmp=$(mktemp)
if ! python3 - "$gsp" "$tmp" <<'PY'
import re, sys
src, dst = sys.argv[1], sys.argv[2]
with open(src) as f:
content = f.read()
# Match the 4-line block exactly as it ships in gamescope-session-git r339.
# Group 1 = leading 3 lines (kept), group 2 = closing `fi` (kept). The new
# elif+body lines are inserted between them.
pattern = re.compile(
r'(\tCUSTOM_REFRESH_RATES_OPTION=""\n'
r'\tif \[ -n "\$CUSTOM_REFRESH_RATES" \] && gamescope_has_option "--custom-refresh-rates"; then\n'
r'\t\tCUSTOM_REFRESH_RATES_OPTION="--custom-refresh-rates \$CUSTOM_REFRESH_RATES"\n'
r')(\tfi\n)'
)
# Use a function as the replacement so re.sub doesn't process backslash escapes
# in our string — we need literal `\n` in the `tr "," "\n"` argument to reach
# the shell, and a string-form replacement would turn it into a real newline.
def _patch(m):
return m.group(1) + (
'\telif [ -n "$CUSTOM_REFRESH_RATES" ] && gamescope_has_option "--nested-refresh"; then # DECKSHIFT-NESTED-REFRESH-FALLBACK\n'
'\t\t_deckshift_rate=$(echo "$CUSTOM_REFRESH_RATES" | tr "," "\\n" | sort -nr | head -1)\n'
'\t\tCUSTOM_REFRESH_RATES_OPTION="--nested-refresh $_deckshift_rate"\n'
) + m.group(2)
new = pattern.sub(_patch, content, count=1)
if new == content:
sys.stderr.write("could not locate CUSTOM_REFRESH_RATES_OPTION block in expected shape\n")
sys.exit(1)
with open(dst, "w") as f:
f.write(new)
PY
then
warn "Could not patch $gsp — upstream may have changed shape"
warn "Refresh-rate selection in the TUI will continue to be a no-op until this is resolved"
rm -f "$tmp"
return 1
fi
if ! grep -q "DECKSHIFT-NESTED-REFRESH-FALLBACK" "$tmp"; then
warn "Patch produced output but marker is missing — aborting install"
rm -f "$tmp"
return 1
fi
sudo install -m 0755 "$tmp" "$gsp"
rm -f "$tmp"
info "Patched $gsp — CUSTOM_REFRESH_RATES now reaches gamescope via --nested-refresh"
}
setup_session_switching() {
echo ""
echo "================================================================"
@ -1344,7 +1430,7 @@ setup_session_switching() {
user_home=$(eval echo "~$current_user")
# GPU detection only — the installer no longer chooses a monitor, resolution
# or refresh rate. Those are user choices, made later via Walker → "DeckShift
# or refresh rate. Those are user choices, made later via the app menu → "DeckShift
# Settings". This avoids stale OUTPUT_CONNECTOR values when displays are
# unplugged and lets the user pick whatever fits their setup.
local -a dgpu_monitors=()
@ -1403,7 +1489,7 @@ setup_session_switching() {
info "Found $dgpu_type on $dgpu_card"
info "Display selection (monitor / resolution / refresh) is left to the user."
info "After install, launch Walker → 'DeckShift Settings' to configure."
info "After install, open the app menu (Super+Space) → 'DeckShift Settings' to configure."
info "Checking for old custom session files to clean up..."
@ -1526,6 +1612,11 @@ setup_session_switching() {
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
fi
# Patch the installed gamescope-session-plus to add --nested-refresh
# fallback. Runs on every install so AUR upgrades that overwrite the file
# get re-patched the next time the user re-runs ./deckshift.sh.
patch_gamescope_session_plus
# NetworkManager Integration
#
# Omarchy uses iwd (Intel Wireless Daemon) for WiFi, but Steam requires
@ -1852,7 +1943,7 @@ UDISKS_POLKIT
# The installer writes only GPU-specific and static keys here. Display keys
# (SCREEN_WIDTH, SCREEN_HEIGHT, CUSTOM_REFRESH_RATES, OUTPUT_CONNECTOR) are
# NOT written by the installer — they are owned by the user and managed via
# the settings TUI (Walker → "DeckShift Settings"). This keeps existing user
# the settings TUI (app menu → "DeckShift Settings"). This keeps existing user
# selections intact across re-runs and avoids preselecting values that may
# not match the user's setup.
#
@ -2219,6 +2310,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
@ -2442,8 +2555,10 @@ KEYBIND_MONITOR
# screencast portal would bind to nothing. We now (a) push live session
# env into D-Bus + systemd --user so portals activate against the new
# Wayland socket, (b) stop portals first, (c) restart pipewire and wait
# for the graph, (d) start portals last, (e) restart Walker/elephant so
# the clipboard listener reattaches to the live Wayland socket too.
# for the graph, (d) start portals last. (Pre-Omarchy-4 versions also
# restarted Walker/elephant to reattach the clipboard listener; Omarchy 4's
# omarchy-shell owns the clipboard and starts fresh with each session, so
# that step is gone.)
info "Creating portal recovery helper..."
local portal_recovery="/usr/local/bin/deckshift-portal-recovery"
@ -2478,18 +2593,28 @@ sleep 2
# Now bring portals up cleanly.
systemctl --user start xdg-desktop-portal-hyprland.service xdg-desktop-portal.service 2>/dev/null || true
# Walker (elephant) holds the clipboard listener, which is also bound to the
# dead Hyprland's Wayland socket and stays broken until restarted.
omarchy-restart-walker >/dev/null 2>&1 || true
PORTAL_RECOVERY
sudo chmod +x "$portal_recovery"
info "Created $portal_recovery"
# Hyprland autostart hook for portal recovery
# Hyprland autostart hook for portal recovery. Omarchy 4's Lua config
# provider ignores autostart.conf, so wire through autostart.lua when it
# exists; fall back to the legacy exec-once on pre-4 installs.
local hypr_autostart_lua="${user_home}/.config/hypr/autostart.lua"
local hypr_autostart="${user_home}/.config/hypr/autostart.conf"
if [[ -f "$hypr_autostart" ]]; then
if [[ -f "$hypr_autostart_lua" ]]; then
if grep -q "deckshift-portal-recovery" "$hypr_autostart_lua" 2>/dev/null; then
info "Portal recovery already wired into autostart.lua"
else
sudo -u "$current_user" tee -a "$hypr_autostart_lua" > /dev/null << 'HYPR_PORTAL_LUA'
-- DeckShift — restart xdg-desktop-portal stack after returning from Gaming Mode
o.launch_on_start("/usr/local/bin/deckshift-portal-recovery")
HYPR_PORTAL_LUA
info "Added portal recovery launch_on_start to $hypr_autostart_lua"
fi
elif [[ -f "$hypr_autostart" ]]; then
if grep -q "deckshift-portal-recovery" "$hypr_autostart" 2>/dev/null; then
info "Portal recovery already wired into autostart.conf"
else
@ -2501,8 +2626,8 @@ HYPR_PORTAL
info "Added portal recovery exec-once to $hypr_autostart"
fi
else
warn "autostart.conf not found at $hypr_autostart — portal recovery not auto-wired"
warn "Add manually: exec-once = /usr/local/bin/deckshift-portal-recovery"
warn "No autostart.lua or autostart.conf under ${user_home}/.config/hypr — portal recovery not auto-wired"
warn "Add manually to autostart.lua: o.launch_on_start(\"/usr/local/bin/deckshift-portal-recovery\")"
fi
# SDDM Session Switching Config
@ -2612,12 +2737,25 @@ SUDOERS_SWITCH
fi
info "Adding Hyprland keybind..."
local hypr_bindings_lua="${user_home}/.config/hypr/bindings.lua"
local hypr_bindings_conf="${user_home}/.config/hypr/bindings.conf"
if [[ ! -f "$hypr_bindings_conf" ]]; then
warn "bindings.conf not found at $hypr_bindings_conf - skipping keybind setup"
warn "You can manually add: bindd = SUPER SHIFT, S, Gaming Mode, exec, /usr/local/bin/switch-to-gaming"
else
if [[ -f "$hypr_bindings_lua" ]]; then
# Omarchy 4: bindings live in Lua; bindings.conf is ignored. Omarchy's
# defaults claim SUPER+SHIFT+S, and duplicate Hyprland binds BOTH fire,
# so the default must be unbound before taking the key.
if grep -q "switch-to-gaming" "$hypr_bindings_lua" 2>/dev/null; then
info "Gaming Mode keybind already exists in bindings.lua"
else
sudo -u "$current_user" tee -a "$hypr_bindings_lua" > /dev/null << 'HYPR_GAMING_LUA'
-- DeckShift — reclaim SUPER+SHIFT+S from the Omarchy default, bind Gaming Mode
hl.unbind("SUPER + SHIFT + S")
o.bind("SUPER + SHIFT + S", "Gaming Mode", "/usr/local/bin/switch-to-gaming")
HYPR_GAMING_LUA
info "Added Gaming Mode keybind to bindings.lua"
fi
elif [[ -f "$hypr_bindings_conf" ]]; then
if grep -q "switch-to-gaming" "$hypr_bindings_conf" 2>/dev/null; then
info "Gaming Mode keybind already exists in bindings.conf"
else
@ -2627,6 +2765,9 @@ bindd = SUPER SHIFT, S, Gaming Mode, exec, /usr/local/bin/switch-to-gaming
HYPR_GAMING
info "Added Gaming Mode keybind to bindings.conf"
fi
else
warn "No bindings.lua or bindings.conf under ${user_home}/.config/hypr - skipping keybind setup"
warn "Add manually to bindings.lua: o.bind(\"SUPER + SHIFT + S\", \"Gaming Mode\", \"/usr/local/bin/switch-to-gaming\")"
fi
info "Steam compatibility scripts provided by gamescope-session-steam-git"
@ -2695,7 +2836,7 @@ HYPR_GAMING
echo " - /usr/local/bin/switch-to-gaming"
echo " - /usr/local/bin/switch-to-desktop"
echo " - /usr/local/bin/gaming-keybind-monitor (Super+Shift+R)"
echo " - ~/.config/hypr/bindings.conf (keybind added)"
echo " - ~/.config/hypr/bindings.lua (keybind added; bindings.conf on pre-Omarchy-4)"
echo ""
echo " NetworkManager integration (Steam network access):"
echo " - /usr/local/bin/gamescope-nm-start"
@ -2785,7 +2926,8 @@ verify_installation() {
["/etc/pipewire/pipewire.conf.d/10-gaming-latency.conf"]="644:PipeWire low-latency"
["/etc/environment.d/99-shader-cache.conf"]="644:Shader cache config"
["/usr/local/bin/deckshift-settings"]="755:Gaming Mode settings TUI"
["/usr/share/applications/deckshift-settings.desktop"]="644:Walker launcher for settings TUI"
["/usr/share/applications/deckshift-settings.desktop"]="644:app menu launcher for settings TUI"
["/usr/share/libalpm/hooks/deckshift-gamescope-cap.hook"]="644:Pacman hook re-applies cap_sys_nice on gamescope upgrade (optional)"
)
echo " FILE STATUS:"
echo " ------------"
@ -2823,8 +2965,16 @@ verify_installation() {
echo ""
echo " HYPRLAND KEYBIND:"
echo " -----------------"
local hypr_bindings_lua="$HOME/.config/hypr/bindings.lua"
local hypr_bindings="$HOME/.config/hypr/bindings.conf"
if [[ -f "$hypr_bindings" ]]; then
if [[ -f "$hypr_bindings_lua" ]]; then
if grep -q "switch-to-gaming" "$hypr_bindings_lua" 2>/dev/null; then
echo " ✓ Gaming Mode keybind (Super+Shift+S) configured in bindings.lua"
else
echo " ✗ Gaming Mode keybind NOT found in bindings.lua (Omarchy 4 ignores bindings.conf)"
all_ok=false
fi
elif [[ -f "$hypr_bindings" ]]; then
if grep -q "switch-to-gaming" "$hypr_bindings" 2>/dev/null; then
echo " ✓ Gaming Mode keybind (Super+Shift+S) configured"
else
@ -2832,7 +2982,26 @@ verify_installation() {
all_ok=false
fi
else
echo " ⚠ bindings.conf not found - keybind needs manual setup"
echo " ⚠ no bindings.lua or bindings.conf - keybind needs manual setup"
fi
echo ""
echo " PORTAL RECOVERY AUTOSTART:"
echo " --------------------------"
local hypr_autostart_lua="$HOME/.config/hypr/autostart.lua"
local hypr_autostart="$HOME/.config/hypr/autostart.conf"
if [[ -f "$hypr_autostart_lua" ]]; then
if grep -q "deckshift-portal-recovery" "$hypr_autostart_lua" 2>/dev/null; then
echo " ✓ portal recovery wired into autostart.lua"
else
echo " ✗ portal recovery NOT wired into autostart.lua (Omarchy 4 ignores autostart.conf)"
all_ok=false
fi
elif [[ -f "$hypr_autostart" ]] && grep -q "deckshift-portal-recovery" "$hypr_autostart" 2>/dev/null; then
echo " ✓ portal recovery wired into autostart.conf"
else
echo " ✗ portal recovery NOT wired into Hyprland autostart"
all_ok=false
fi
echo ""
@ -3010,7 +3179,7 @@ verify_installation() {
# 6. Install script requirements and performance permissions
# 7. Set up session switching (the big one — all the scripts and configs)
# 8. Optionally install Xbox Bluetooth controller support (xpadneo)
# 9. Install the Gaming Mode settings TUI + Walker launcher
# 9. Install the Gaming Mode settings TUI + app menu launcher
# 10. Prompt for reboot/relogin if needed
# 11. Optionally run verification to confirm everything worked
execute_setup() {