Compare commits
10
Commits
8293d26c07
...
835a939a81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835a939a81 | ||
|
|
0844f02a8c | ||
|
|
d45f81be36 | ||
|
|
a3cb6f28e4 | ||
|
|
52c883b780 | ||
|
|
aaa2f3d768 | ||
|
|
3131dcab8f | ||
|
|
4fa77a6238 | ||
|
|
ba86b8c777 | ||
|
|
e54f45723f |
@@ -1,15 +1,72 @@
|
|||||||
# DeckShift
|
# DeckShift
|
||||||
|
|
||||||
**Version 0.1.3** — Steam Deck-style gaming mode for Linux + Hyprland. Press `Super+Shift+S` to enter Gaming Mode (Steam Big Picture in Gamescope), `Super+Shift+R` to return to your desktop.
|
**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.
|
||||||
|
|
||||||
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 as the project moves toward distro-portability.
|
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.
|
||||||
|
|
||||||
> **Current status**: targets [Omarchy](https://omarchy.com) (Arch + Hyprland + SDDM + iwd). Works on other Arch + Hyprland setups with minor manual tweaks. Cross-distro support (Fedora / openSUSE / Cachy) is the next direction.
|
> **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.
|
||||||
|
|
||||||
[](https://youtu.be/nj4pLh3spCs)
|
[](https://youtu.be/nj4pLh3spCs)
|
||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- Panel width is adaptive (`min(terminal − 6, 60)`, floored at 40) so the layout looks right from narrow ttys up to fullscreen.
|
||||||
|
- Terminal-width detection now reads `stty size </dev/tty` first (kernel-reported, always reflects the live window) and only falls back to `tput cols` / `80`. Fixes off-centre rendering in freshly-spawned floating terminals whose terminfo hasn't caught up yet.
|
||||||
|
- Config-file path now renders with `~` instead of `/home/<user>/…` so it fits the panel.
|
||||||
|
- Unset resolution shows `<auto>` (matching the other unset placeholders) instead of `?x?`.
|
||||||
|
|
||||||
|
### v0.1.9 — Auto-migrate legacy refresh-rate values
|
||||||
|
|
||||||
|
- Installer now detects pre-v0.1.8 scalar `CUSTOM_REFRESH_RATES` values (e.g. `165`) and rewrites them to the v0.1.8 comma format (`60,165`), then imports the new value into the running systemd user environment. Re-running `./deckshift.sh` is enough to fix Gaming Mode for users hit by the 60 Hz bug — no need to re-open the Settings TUI and re-pick the rate.
|
||||||
|
|
||||||
|
### v0.1.8 — Settings TUI now reaches gamescope without re-login
|
||||||
|
|
||||||
|
- The Settings TUI used to write `~/.config/environment.d/gamescope-session-plus.conf` and rely on the user logging out before the change reached `gamescope-session-plus@.service`. Saving the TUI now calls `systemctl --user import-environment` for the keys it just wrote, so the next Gaming Mode launch picks up the new values immediately.
|
||||||
|
- Refresh-rate writes are now a comma list with `60` as the floor (e.g. `60,165`) rather than a single value. Gamescope's `--custom-refresh-rates` is a list of switchable rates, not a launch-rate selector — keeping `60` in the list guarantees a safe fallback if the high-rate mode isn't enumerated on first launch.
|
||||||
|
- Fixes a reported regression where Gaming Mode always launched at 60 Hz on NVIDIA + HDMI even though the TUI showed the user's chosen rate.
|
||||||
|
|
||||||
|
### v0.1.7 — Foot terminal compatibility
|
||||||
|
|
||||||
|
- Internal: confirmed DeckShift Settings TUI works unchanged with Omarchy's new `foot` terminal (in addition to kitty / ghostty / alacritty). No code changes required — Omarchy's stock floating-window rule already lists foot's native class.
|
||||||
|
|
||||||
|
### v0.1.6 — Omarchy-only, simpler portal recovery
|
||||||
|
|
||||||
|
- Dropped the non-Omarchy fallback in `deckshift-portal-recovery` — DeckShift targets Omarchy only, so the helper now just calls `omarchy-restart-walker` directly.
|
||||||
|
- Header / docs cleaned up to drop the "cross-distro is the next direction" note.
|
||||||
|
|
||||||
|
### v0.1.5 — clipboard recovery after Gaming Mode
|
||||||
|
|
||||||
|
- After returning from Gaming Mode, Walker's clipboard listener (`elephant.service`) was still bound to the killed Hyprland's Wayland socket, so paste did nothing and clipboard history was empty.
|
||||||
|
- `deckshift-portal-recovery` now calls `omarchy-restart-walker` at the end, which restarts `elephant.service` + `app-walker@autostart.service` and reattaches the clipboard to the live compositor.
|
||||||
|
|
||||||
|
### v0.1.4 — portal recovery race fix
|
||||||
|
|
||||||
|
- The initial `deckshift-portal-recovery` helper restarted all five services (xdg-desktop-portal-hyprland, xdg-desktop-portal, pipewire, pipewire-pulse, wireplumber) simultaneously. That raced — the portals could come up before wireplumber had rebuilt the node graph, leaving the screencast portal bound to nothing.
|
||||||
|
- Rewritten as a serialised sequence: push live `WAYLAND_DISPLAY`/`XDG_*` env into systemd-user + D-Bus activation env → stop portals → SIGTERM/SIGKILL stragglers → restart pipewire stack → wait → start portals.
|
||||||
|
- Thanks to the user on the issue tracker who diagnosed the race and supplied the env-update + serialised-restart sequence.
|
||||||
|
|
||||||
### v0.1.3 — power-state save/restore + reliable exit
|
### v0.1.3 — power-state save/restore + reliable exit
|
||||||
|
|
||||||
- **Saves your real pre-Gaming-Mode state** (CPU governor + power profile) on entry to `~/.cache/deckshift/saved-state` and restores those exact values on exit. No more guessing `powersave`/`balanced`.
|
- **Saves your real pre-Gaming-Mode state** (CPU governor + power profile) on entry to `~/.cache/deckshift/saved-state` and restores those exact values on exit. No more guessing `powersave`/`balanced`.
|
||||||
@@ -431,17 +488,17 @@ 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`
|
- 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`
|
- 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)**
|
**Screen sharing in Chromium / Firefox is broken after returning from Gaming Mode (only "Share a tab" works) — and/or clipboard is dead**
|
||||||
|
|
||||||
`xdg-desktop-portal-hyprland` is bound to the killed Hyprland instance — tab capture works because it bypasses the portal, but desktop/window capture goes through it and fails silently.
|
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.
|
||||||
|
|
||||||
DeckShift handles this automatically via `/usr/local/bin/deckshift-portal-recovery` (autostarted from `~/.config/hypr/autostart.conf`). If you installed before this fix or the autostart hook didn't get added, run:
|
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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user restart xdg-desktop-portal-hyprland xdg-desktop-portal pipewire pipewire-pulse wireplumber
|
touch /tmp/.deckshift-just-returned && /usr/local/bin/deckshift-portal-recovery
|
||||||
```
|
```
|
||||||
|
|
||||||
then re-open the browser tab.
|
then re-open the browser tab. (The `touch` is needed because the helper is a no-op without the marker file — that's deliberate, so it doesn't bounce portals on every normal login.)
|
||||||
|
|
||||||
**Suspend fails with "Access denied" after returning from Gaming Mode**
|
**Suspend fails with "Access denied" after returning from Gaming Mode**
|
||||||
|
|
||||||
@@ -461,6 +518,18 @@ If you're on AC and using Omarchy, this is expected — see the *Performance Mod
|
|||||||
- Older Gen8/9 Intel iGPUs (Skylake, Kaby Lake) struggle with Vulkan workloads. Lower the launch resolution via the Settings TUI (`deckshift-settings`) — 720p / 1080p makes a big difference.
|
- Older Gen8/9 Intel iGPUs (Skylake, Kaby Lake) struggle with Vulkan workloads. Lower the launch resolution via the Settings TUI (`deckshift-settings`) — 720p / 1080p makes a big difference.
|
||||||
- If you have a discrete GPU that should take over, check its driver is loaded: `lspci -k | grep -A2 VGA`
|
- If you have a discrete GPU that should take over, check its driver is loaded: `lspci -k | grep -A2 VGA`
|
||||||
|
|
||||||
|
**Gaming Mode launches at 60 Hz even though I picked a higher rate in the TUI**
|
||||||
|
|
||||||
|
`--custom-refresh-rates` is gamescope's list of *switchable* rates, not a launch-rate selector. On embedded/DRM output (especially NVIDIA + HDMI) gamescope picks the connector's EDID-preferred mode at first launch, which is usually 60 Hz even when higher modes are enumerated. Two-step fix:
|
||||||
|
|
||||||
|
1. Confirm the env var actually reached the session:
|
||||||
|
```bash
|
||||||
|
systemctl --user show-environment | grep REFRESH
|
||||||
|
journalctl --user -u "gamescope-session-plus@*" -b --no-pager | grep -m1 -- '--custom-refresh-rates'
|
||||||
|
```
|
||||||
|
In v0.1.8+ this should work without re-login — the Settings TUI now calls `systemctl --user import-environment` on save. If you're on an older release, log out and back in once after saving in the TUI.
|
||||||
|
2. Once Steam Big Picture is up, set the rate explicitly: Settings → Display → Refresh Rate → your rate. Steam persists this client-side, so every subsequent Gaming Mode launch will go straight to that rate.
|
||||||
|
|
||||||
### Log Locations
|
### Log Locations
|
||||||
|
|
||||||
| Component | Command |
|
| Component | Command |
|
||||||
|
|||||||
+198
-24
@@ -48,6 +48,80 @@ command -v lspci >/dev/null || die "lspci is required"
|
|||||||
command -v jq >/dev/null || die "jq is required (install with: omarchy-pkg-add jq)"
|
command -v jq >/dev/null || die "jq is required (install with: omarchy-pkg-add jq)"
|
||||||
command -v hyprctl >/dev/null || die "hyprctl is required (this TUI is for Hyprland sessions)"
|
command -v hyprctl >/dev/null || die "hyprctl is required (this TUI is for Hyprland sessions)"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Layout — everything renders inside a single centred "panel" column so the
|
||||||
|
# banner, state, menu, and toasts all share a left edge regardless of terminal
|
||||||
|
# width. PANEL_WIDTH is adaptive: target 60 cols, capped at the terminal width
|
||||||
|
# minus margin, floored so it stays usable on narrow windows.
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Live terminal width — refreshed each main loop iteration so a window resize
|
||||||
|
# is picked up between menus. Reads via `stty size` first (kernel-reported,
|
||||||
|
# always reflects the live window) because `tput cols` in a freshly-spawned
|
||||||
|
# floating terminal sometimes returns the terminfo default (80) before the
|
||||||
|
# compositor has applied its size.
|
||||||
|
#
|
||||||
|
# Every `$(...)` here ends with `|| true` because under `set -eo pipefail` a
|
||||||
|
# failing pipeline inside a command substitution propagates out and trips -e
|
||||||
|
# on the enclosing assignment.
|
||||||
|
COLS=80
|
||||||
|
PANEL_WIDTH=60
|
||||||
|
LEFT_MARGIN=0
|
||||||
|
LEFT_PAD=""
|
||||||
|
|
||||||
|
refresh_cols() {
|
||||||
|
local raw
|
||||||
|
raw=$(stty size 2>/dev/null </dev/tty || true)
|
||||||
|
if [[ "$raw" =~ ^[0-9]+[[:space:]]+([0-9]+) ]] && (( BASH_REMATCH[1] > 0 )); then
|
||||||
|
COLS=${BASH_REMATCH[1]}
|
||||||
|
else
|
||||||
|
raw=$(tput cols 2>/dev/null || true)
|
||||||
|
if [[ "$raw" =~ ^[0-9]+$ ]] && (( raw > 0 )); then
|
||||||
|
COLS=$raw
|
||||||
|
else
|
||||||
|
COLS=80
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
PANEL_WIDTH=60
|
||||||
|
(( PANEL_WIDTH > COLS - 6 )) && PANEL_WIDTH=$(( COLS - 6 ))
|
||||||
|
(( PANEL_WIDTH < 40 )) && PANEL_WIDTH=40
|
||||||
|
LEFT_MARGIN=$(( (COLS - PANEL_WIDTH) / 2 ))
|
||||||
|
(( LEFT_MARGIN < 0 )) && LEFT_MARGIN=0
|
||||||
|
LEFT_PAD=""
|
||||||
|
(( LEFT_MARGIN > 0 )) && printf -v LEFT_PAD '%*s' "$LEFT_MARGIN" ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepend N spaces to every line of stdin. Used to position blocks at the
|
||||||
|
# panel's left edge so banner, state, menu, and toasts share one column.
|
||||||
|
pad_block() {
|
||||||
|
local n="${1:-$LEFT_MARGIN}"
|
||||||
|
local pad=""
|
||||||
|
(( n > 0 )) && printf -v pad '%*s' "$n" ''
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
printf '%s%s\n' "$pad" "$line"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Centred wrapper around gum choose: positions the menu horizontally so it
|
||||||
|
# sits inside the panel column. gum draws at column 0 with no alignment flag,
|
||||||
|
# but it reserves the cursor string's width as the gutter for unselected rows,
|
||||||
|
# so baking padding into the cursor prefix shifts the whole rendered block.
|
||||||
|
# Header is padded separately so it sits above the items at the same offset.
|
||||||
|
#
|
||||||
|
# Usage: cmenu "Header text" "Item 1" "Item 2" ...
|
||||||
|
# Cancellation (Esc/Ctrl-C) returns empty just like gchoose.
|
||||||
|
cmenu() {
|
||||||
|
refresh_cols
|
||||||
|
local header="$1"; shift
|
||||||
|
# Left-align the menu at the panel's left edge so the "> " cursor and item
|
||||||
|
# labels sit in the same column as the state panel's section headers and
|
||||||
|
# rows above. Centering items within the panel would put them in the middle
|
||||||
|
# of the panel — visually disconnected from the state panel column.
|
||||||
|
local pad="$LEFT_PAD"
|
||||||
|
gchoose --cursor "${pad}> " --header "${pad}${header}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Conf helpers — read straight from disk; writes go through pending_* below so
|
# Conf helpers — read straight from disk; writes go through pending_* below so
|
||||||
# nothing hits the file until the user explicitly saves.
|
# nothing hits the file until the user explicitly saves.
|
||||||
@@ -109,6 +183,19 @@ flush_pending() {
|
|||||||
echo "${key}=${value}" >> "$CONF"
|
echo "${key}=${value}" >> "$CONF"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# environment.d/*.conf is parsed by `systemd --user` at user-manager startup,
|
||||||
|
# so edits made here don't reach gamescope-session-plus@.service until the
|
||||||
|
# next login unless we nudge systemd. import-environment pulls the keys we
|
||||||
|
# just wrote into the running user manager scope so the next Gaming Mode
|
||||||
|
# launch sees them without a re-login.
|
||||||
|
if [[ ${#PENDING_SET[@]} -gt 0 ]]; then
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
systemctl --user import-environment $(printf '%s ' "${!PENDING_SET[@]}") 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ ${#PENDING_UNSET[@]} -gt 0 ]]; then
|
||||||
|
systemctl --user unset-environment "${!PENDING_UNSET[@]}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -220,15 +307,22 @@ list_gpus() {
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
show_state() {
|
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)
|
connector=$(effective OUTPUT_CONNECTOR)
|
||||||
width=$(effective SCREEN_WIDTH)
|
width=$(effective SCREEN_WIDTH)
|
||||||
height=$(effective SCREEN_HEIGHT)
|
height=$(effective SCREEN_HEIGHT)
|
||||||
refresh=$(effective CUSTOM_REFRESH_RATES)
|
refresh=$(effective CUSTOM_REFRESH_RATES)
|
||||||
|
# Stored as a comma list (e.g. "60,165") so gamescope has a safe 60 Hz
|
||||||
|
# fallback. The user only cares about the rate they picked — the highest
|
||||||
|
# member of the list.
|
||||||
|
if [[ "$refresh" == *,* ]]; then
|
||||||
|
refresh=$(tr ',' '\n' <<<"$refresh" | sort -nr | head -1)
|
||||||
|
fi
|
||||||
vk_adapter=$(effective VULKAN_ADAPTER)
|
vk_adapter=$(effective VULKAN_ADAPTER)
|
||||||
dri_prime=$(effective DRI_PRIME)
|
dri_prime=$(effective DRI_PRIME)
|
||||||
prime_offload=$(effective __NV_PRIME_RENDER_OFFLOAD)
|
prime_offload=$(effective __NV_PRIME_RENDER_OFFLOAD)
|
||||||
mesa_vk_select=$(effective MESA_VK_DEVICE_SELECT)
|
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,
|
# 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
|
# since the modes are mutually exclusive. AMD hybrid is identified by both
|
||||||
@@ -253,15 +347,26 @@ show_state() {
|
|||||||
monitor_label="${monitor_label} (max ${HYPR_NATIVE} @ ${HYPR_MAX_REFRESH:-?}Hz)"
|
monitor_label="${monitor_label} (max ${HYPR_NATIVE} @ ${HYPR_MAX_REFRESH:-?}Hz)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Build resolution label so an unset value renders as "<auto>" (matching the
|
||||||
|
# other unset placeholders) rather than "?x?".
|
||||||
|
local resolution_label="<auto>"
|
||||||
|
if [[ -n "$width" && -n "$height" ]]; then
|
||||||
|
resolution_label="${width}x${height}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Replace $HOME with ~ so the config path fits the panel column.
|
||||||
|
local conf_display="${CONF/#$HOME/\~}"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Gaming Mode display settings${pending_label}:
|
Gaming Mode display settings${pending_label}
|
||||||
|
|
||||||
Monitor : ${monitor_label}
|
Monitor : ${monitor_label}
|
||||||
Resolution : ${width:-?}x${height:-?}
|
Resolution : ${resolution_label}
|
||||||
Refresh rate : ${refresh:-<auto>} Hz
|
Refresh rate : ${refresh:-<auto>} Hz
|
||||||
GPU mode : ${gpu_mode}
|
GPU mode : ${gpu_mode}
|
||||||
|
Hide monitor : ${disable_connector:-<none>}
|
||||||
|
|
||||||
Config file : ${CONF}
|
Config file : ${conf_display}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +389,7 @@ choose_monitor() {
|
|||||||
pending_set OUTPUT_CONNECTOR "$connector"
|
pending_set OUTPUT_CONNECTOR "$connector"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select monitor for Gaming Mode")
|
choice=$(cmenu "Select monitor for Gaming Mode" "${labels[@]}")
|
||||||
[[ -z "$choice" ]] && return 0
|
[[ -z "$choice" ]] && return 0
|
||||||
if [[ "$choice" == "(clear"* ]]; then
|
if [[ "$choice" == "(clear"* ]]; then
|
||||||
pending_unset OUTPUT_CONNECTOR
|
pending_unset OUTPUT_CONNECTOR
|
||||||
@@ -293,6 +398,49 @@ choose_monitor() {
|
|||||||
fi
|
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() {
|
choose_resolution() {
|
||||||
refresh_monitor_data
|
refresh_monitor_data
|
||||||
local choice w h
|
local choice w h
|
||||||
@@ -322,7 +470,7 @@ choose_resolution() {
|
|||||||
|
|
||||||
options+=("Custom…")
|
options+=("Custom…")
|
||||||
|
|
||||||
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select launch resolution (max: ${HYPR_NATIVE:-unknown})")
|
choice=$(cmenu "Select launch resolution (max: ${HYPR_NATIVE:-unknown})" "${options[@]}")
|
||||||
[[ -z "$choice" ]] && return 0
|
[[ -z "$choice" ]] && return 0
|
||||||
if [[ "$choice" == "Custom…" ]]; then
|
if [[ "$choice" == "Custom…" ]]; then
|
||||||
w=$(ginput --prompt "Width: " --placeholder "2560")
|
w=$(ginput --prompt "Width: " --placeholder "2560")
|
||||||
@@ -377,7 +525,7 @@ choose_refresh_rate() {
|
|||||||
|
|
||||||
options+=("Custom…")
|
options+=("Custom…")
|
||||||
|
|
||||||
choice=$(printf '%s\n' "${options[@]}" | gchoose --header "Select refresh rate (Hz, max: ${HYPR_MAX_REFRESH:-unknown})")
|
choice=$(cmenu "Select refresh rate (Hz, max: ${HYPR_MAX_REFRESH:-unknown})" "${options[@]}")
|
||||||
[[ -z "$choice" ]] && return 0
|
[[ -z "$choice" ]] && return 0
|
||||||
if [[ "$choice" == "Custom…" ]]; then
|
if [[ "$choice" == "Custom…" ]]; then
|
||||||
rate=$(ginput --prompt "Rate (Hz): " --placeholder "144")
|
rate=$(ginput --prompt "Rate (Hz): " --placeholder "144")
|
||||||
@@ -385,7 +533,17 @@ choose_refresh_rate() {
|
|||||||
rate=${choice%% *}
|
rate=${choice%% *}
|
||||||
fi
|
fi
|
||||||
[[ -z "$rate" ]] && return 0
|
[[ -z "$rate" ]] && return 0
|
||||||
pending_set CUSTOM_REFRESH_RATES "$rate"
|
|
||||||
|
# gamescope-session-plus passes this env value to gamescope as
|
||||||
|
# --custom-refresh-rates, which is a list of *switchable* rates the user can
|
||||||
|
# cycle between in Steam Big Picture. Writing a single value (e.g. "165") can
|
||||||
|
# leave gamescope without a safe 60 Hz fallback if the monitor's EDID
|
||||||
|
# preferred mode is 60 Hz and the high-rate mode isn't enumerated on first
|
||||||
|
# launch — most visibly on NVIDIA HDMI outputs. Always include 60 as the
|
||||||
|
# floor so Steam can fall back and the user can switch up to their pick.
|
||||||
|
local rate_list="$rate"
|
||||||
|
[[ "$rate" != "60" ]] && rate_list="60,${rate}"
|
||||||
|
pending_set CUSTOM_REFRESH_RATES "$rate_list"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keys this menu owns. Cleared at the start of every selection so switching
|
# Keys this menu owns. Cleared at the start of every selection so switching
|
||||||
@@ -444,7 +602,7 @@ choose_gpu() {
|
|||||||
done
|
done
|
||||||
labels+=("(clear GPU override — let system decide)")
|
labels+=("(clear GPU override — let system decide)")
|
||||||
|
|
||||||
choice=$(printf '%s\n' "${labels[@]}" | gchoose --header "Select GPU for Gaming Mode")
|
choice=$(cmenu "Select GPU for Gaming Mode" "${labels[@]}")
|
||||||
[[ -z "$choice" ]] && return 0
|
[[ -z "$choice" ]] && return 0
|
||||||
|
|
||||||
# Wipe every GPU-mode key first; each branch sets only what it needs.
|
# Wipe every GPU-mode key first; each branch sets only what it needs.
|
||||||
@@ -471,7 +629,7 @@ choose_gpu() {
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
local t_choice t_entry=""
|
local t_choice t_entry=""
|
||||||
t_choice=$(printf '%s\n' "${t_labels[@]}" | gchoose --header "Pick dGPU (render target) — usually the discrete one")
|
t_choice=$(cmenu "Pick dGPU (render target) — usually the discrete one" "${t_labels[@]}")
|
||||||
[[ -z "$t_choice" ]] && return 0
|
[[ -z "$t_choice" ]] && return 0
|
||||||
local i
|
local i
|
||||||
for ((i=0; i<${#t_labels[@]}; i++)); do
|
for ((i=0; i<${#t_labels[@]}; i++)); do
|
||||||
@@ -526,6 +684,11 @@ confirm_risky_save() {
|
|||||||
w=$(effective SCREEN_WIDTH)
|
w=$(effective SCREEN_WIDTH)
|
||||||
h=$(effective SCREEN_HEIGHT)
|
h=$(effective SCREEN_HEIGHT)
|
||||||
rate=$(effective CUSTOM_REFRESH_RATES)
|
rate=$(effective CUSTOM_REFRESH_RATES)
|
||||||
|
# Check the user's actual pick (highest of the comma list) against monitor
|
||||||
|
# support — not the bare list, which won't match a single mode line.
|
||||||
|
if [[ "$rate" == *,* ]]; then
|
||||||
|
rate=$(tr ',' '\n' <<<"$rate" | sort -nr | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
local -a warnings=()
|
local -a warnings=()
|
||||||
if [[ -n "$w" && -n "$h" ]] && ! resolution_supported "$w" "$h"; then
|
if [[ -n "$w" && -n "$h" ]] && ! resolution_supported "$w" "$h"; then
|
||||||
@@ -537,12 +700,14 @@ confirm_risky_save() {
|
|||||||
|
|
||||||
(( ${#warnings[@]} == 0 )) && return 0
|
(( ${#warnings[@]} == 0 )) && return 0
|
||||||
|
|
||||||
|
refresh_cols
|
||||||
clear
|
clear
|
||||||
gum style --foreground 196 --bold "Warning — selected values may not work:"
|
|
||||||
echo ""
|
echo ""
|
||||||
printf ' %s\n' "${warnings[@]}"
|
gum style --foreground 196 --bold "Warning — selected values may not work:" | pad_block
|
||||||
echo ""
|
echo ""
|
||||||
gum style --foreground 244 "If Gaming Mode shows a black screen, press Super+Shift+R to return to desktop."
|
printf ' %s\n' "${warnings[@]}" | pad_block
|
||||||
|
echo ""
|
||||||
|
gum style --foreground 244 "If Gaming Mode shows a black screen, press Super+Shift+R to return to desktop." | pad_block
|
||||||
echo ""
|
echo ""
|
||||||
gum confirm "Save anyway?" --default=false
|
gum confirm "Save anyway?" --default=false
|
||||||
}
|
}
|
||||||
@@ -553,12 +718,17 @@ confirm_risky_save() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
while true; do
|
while true; do
|
||||||
|
refresh_cols
|
||||||
refresh_monitor_data
|
refresh_monitor_data
|
||||||
clear
|
clear
|
||||||
|
echo ""
|
||||||
gum style \
|
gum style \
|
||||||
--border double --margin "1" --padding "1 4" --border-foreground 212 \
|
--border double --padding "1 0" --border-foreground 212 \
|
||||||
"DECKSHIFT — Gaming Mode Settings"
|
--width "$PANEL_WIDTH" --align center \
|
||||||
show_state
|
"DECKSHIFT — Gaming Mode Settings" \
|
||||||
|
| pad_block
|
||||||
|
echo ""
|
||||||
|
show_state | pad_block
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
local save_label="Save and exit"
|
local save_label="Save and exit"
|
||||||
@@ -569,26 +739,29 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local action
|
local action
|
||||||
action=$(gchoose --header "What do you want to change?" \
|
action=$(cmenu "What do you want to change?" \
|
||||||
"Monitor" \
|
"Monitor" \
|
||||||
"Resolution" \
|
"Resolution" \
|
||||||
"Refresh rate" \
|
"Refresh rate" \
|
||||||
"GPU" \
|
"GPU" \
|
||||||
|
"Hide monitor" \
|
||||||
"$save_label" \
|
"$save_label" \
|
||||||
"$cancel_label")
|
"$cancel_label")
|
||||||
case "$action" in
|
case "$action" in
|
||||||
"Monitor") choose_monitor ;;
|
"Monitor") choose_monitor ;;
|
||||||
"Resolution") choose_resolution ;;
|
"Resolution") choose_resolution ;;
|
||||||
"Refresh rate") choose_refresh_rate ;;
|
"Refresh rate") choose_refresh_rate ;;
|
||||||
"GPU") choose_gpu ;;
|
"GPU") choose_gpu ;;
|
||||||
|
"Hide monitor") choose_monitor_to_disable ;;
|
||||||
"Save and exit"*)
|
"Save and exit"*)
|
||||||
if ! confirm_risky_save; then
|
if ! confirm_risky_save; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
flush_pending
|
flush_pending
|
||||||
clear
|
clear
|
||||||
gum style --foreground 212 "Settings saved to $CONF"
|
echo ""
|
||||||
gum style --foreground 244 "Changes apply next time you enter Gaming Mode (Super+Shift+S)."
|
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
|
sleep 1
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
@@ -597,7 +770,8 @@ main() {
|
|||||||
gum confirm "Discard unsaved changes?" --default=true || continue
|
gum confirm "Discard unsaved changes?" --default=true || continue
|
||||||
fi
|
fi
|
||||||
clear
|
clear
|
||||||
gum style --foreground 244 "No changes saved."
|
echo ""
|
||||||
|
gum style --foreground 244 "No changes saved." | pad_block
|
||||||
sleep 1
|
sleep 1
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
|||||||
+191
-23
@@ -34,7 +34,7 @@ set -Euo pipefail
|
|||||||
# -u: Treat unset variables as errors (catches typos in variable names)
|
# -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
|
# -o pipefail: A pipeline fails if ANY command in it fails, not just the last one
|
||||||
|
|
||||||
DECKSHIFT_VERSION="0.1.6"
|
DECKSHIFT_VERSION="0.1.13"
|
||||||
|
|
||||||
# Resolve the directory this script lives in so we can find sibling files like
|
# Resolve the directory this script lives in so we can find sibling files like
|
||||||
# bin/deckshift-settings and applications/deckshift-settings.desktop when
|
# bin/deckshift-settings and applications/deckshift-settings.desktop when
|
||||||
@@ -493,12 +493,10 @@ check_steam_dependencies() {
|
|||||||
local -a optional_deps=()
|
local -a optional_deps=()
|
||||||
|
|
||||||
local -a core_deps=(
|
local -a core_deps=(
|
||||||
"steam"
|
|
||||||
"lib32-vulkan-icd-loader"
|
"lib32-vulkan-icd-loader"
|
||||||
"vulkan-icd-loader"
|
"vulkan-icd-loader"
|
||||||
"lib32-mesa"
|
"lib32-mesa"
|
||||||
"mesa"
|
"mesa"
|
||||||
"mesa-utils"
|
|
||||||
"lib32-glibc"
|
"lib32-glibc"
|
||||||
"lib32-gcc-libs"
|
"lib32-gcc-libs"
|
||||||
"lib32-libx11"
|
"lib32-libx11"
|
||||||
@@ -547,7 +545,6 @@ check_steam_dependencies() {
|
|||||||
info "NVIDIA driver branch: modern (nvidia-utils)"
|
info "NVIDIA driver branch: modern (nvidia-utils)"
|
||||||
gpu_deps+=(
|
gpu_deps+=(
|
||||||
"nvidia-utils"
|
"nvidia-utils"
|
||||||
"lib32-nvidia-utils"
|
|
||||||
"nvidia-settings"
|
"nvidia-settings"
|
||||||
"libva-nvidia-driver"
|
"libva-nvidia-driver"
|
||||||
)
|
)
|
||||||
@@ -559,7 +556,6 @@ check_steam_dependencies() {
|
|||||||
info "NVIDIA driver branch: legacy 580xx (Maxwell/Pascal/Volta)"
|
info "NVIDIA driver branch: legacy 580xx (Maxwell/Pascal/Volta)"
|
||||||
gpu_deps+=(
|
gpu_deps+=(
|
||||||
"nvidia-580xx-utils"
|
"nvidia-580xx-utils"
|
||||||
"lib32-nvidia-580xx-utils"
|
|
||||||
"nvidia-settings"
|
"nvidia-settings"
|
||||||
"libva-nvidia-driver"
|
"libva-nvidia-driver"
|
||||||
)
|
)
|
||||||
@@ -571,7 +567,6 @@ check_steam_dependencies() {
|
|||||||
info "NVIDIA detected but driver branch unrecognised; defaulting to modern (nvidia-utils)"
|
info "NVIDIA detected but driver branch unrecognised; defaulting to modern (nvidia-utils)"
|
||||||
gpu_deps+=(
|
gpu_deps+=(
|
||||||
"nvidia-utils"
|
"nvidia-utils"
|
||||||
"lib32-nvidia-utils"
|
|
||||||
"nvidia-settings"
|
"nvidia-settings"
|
||||||
"libva-nvidia-driver"
|
"libva-nvidia-driver"
|
||||||
)
|
)
|
||||||
@@ -581,17 +576,14 @@ check_steam_dependencies() {
|
|||||||
if $has_amd; then
|
if $has_amd; then
|
||||||
gpu_deps+=(
|
gpu_deps+=(
|
||||||
"vulkan-radeon"
|
"vulkan-radeon"
|
||||||
"lib32-vulkan-radeon"
|
|
||||||
"libvdpau"
|
"libvdpau"
|
||||||
"lib32-libvdpau"
|
"lib32-libvdpau"
|
||||||
)
|
)
|
||||||
! check_package "xf86-video-amdgpu" && optional_deps+=("xf86-video-amdgpu")
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $has_intel; then
|
if $has_intel; then
|
||||||
gpu_deps+=(
|
gpu_deps+=(
|
||||||
"vulkan-intel"
|
"vulkan-intel"
|
||||||
"lib32-vulkan-intel"
|
|
||||||
"intel-media-driver"
|
"intel-media-driver"
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
@@ -778,14 +770,15 @@ check_steam_dependencies() {
|
|||||||
|
|
||||||
check_steam_config
|
check_steam_config
|
||||||
|
|
||||||
# Bootstrap Steam — launches it in the background so its first-run client
|
# Bootstrap Steam via Omarchy's installer — idempotent and the canonical
|
||||||
# update happens in parallel with the rest of the install. Same pattern as
|
# path on AMD/Intel/NVIDIA. Handles lib32 GPU drivers + setsid gtk-launch
|
||||||
# omarchy-install-gaming-steam. Skipped silently if Steam isn't installed
|
# in one shot; previous homegrown gtk-launch call silently no-op'd on
|
||||||
# (e.g. user declined to install missing required deps).
|
# AMD systems that lacked gtk3.
|
||||||
if check_package steam && command -v gtk-launch >/dev/null 2>&1; then
|
if command -v omarchy-install-gaming-steam >/dev/null 2>&1; then
|
||||||
info "Launching Steam to complete its first-run download..."
|
info "Bootstrapping Steam via omarchy-install-gaming-steam..."
|
||||||
setsid gtk-launch steam >/dev/null 2>&1 < /dev/null &
|
omarchy-install-gaming-steam || warn "omarchy-install-gaming-steam returned non-zero"
|
||||||
disown 2>/dev/null || true
|
else
|
||||||
|
warn "omarchy-install-gaming-steam not found — skipping Steam bootstrap"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1137,7 @@ restart_elephant_walker() {
|
|||||||
# After installing packages, it also runs the sub-setup functions for
|
# After installing packages, it also runs the sub-setup functions for
|
||||||
# performance permissions, shader cache, and gamescope capabilities.
|
# performance permissions, shader cache, and gamescope capabilities.
|
||||||
setup_requirements() {
|
setup_requirements() {
|
||||||
local -a required_packages=("steam" "gamescope" "mangohud" "python" "python-evdev" "libcap" "gamemode" "curl" "pciutils" "ntfs-3g" "xcb-util-cursor")
|
local -a required_packages=("gamescope" "mangohud" "python" "python-evdev" "libcap" "gamemode" "curl" "pciutils" "ntfs-3g" "xcb-util-cursor")
|
||||||
local -a packages_to_install=()
|
local -a packages_to_install=()
|
||||||
for pkg in "${required_packages[@]}"; do
|
for pkg in "${required_packages[@]}"; do
|
||||||
check_package "$pkg" || packages_to_install+=("$pkg")
|
check_package "$pkg" || packages_to_install+=("$pkg")
|
||||||
@@ -1169,24 +1162,64 @@ setup_requirements() {
|
|||||||
configure_elephant_launcher
|
configure_elephant_launcher
|
||||||
|
|
||||||
if [[ "${PERFORMANCE_MODE,,}" == "enabled" ]] && command -v gamescope >/dev/null 2>&1; then
|
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 "================================================================"
|
echo "================================================================"
|
||||||
echo " GAMESCOPE CAPABILITY REQUEST"
|
echo " GAMESCOPE CAPABILITY REQUEST"
|
||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
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 ""
|
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
|
echo
|
||||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||||
sudo setcap 'cap_sys_nice=eip' "$(command -v gamescope)" || warn "Failed to set capability"
|
if $needs_cap; then
|
||||||
info "Capability granted to gamescope"
|
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
|
fi
|
||||||
|
else
|
||||||
|
install_gamescope_cap_hook
|
||||||
fi
|
fi
|
||||||
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).
|
# Optional: install Bluetooth Xbox controller support (xpadneo).
|
||||||
# Wired Xbox controllers work without this via the kernel's xpad driver.
|
# Wired Xbox controllers work without this via the kernel's xpad driver.
|
||||||
# xpadneo-dkms gives proper button mapping and rumble for wireless controllers
|
# xpadneo-dkms gives proper button mapping and rumble for wireless controllers
|
||||||
@@ -1309,6 +1342,95 @@ setup_settings_tui() {
|
|||||||
# It also installs ChimeraOS's gamescope-session packages from AUR, which
|
# It also installs ChimeraOS's gamescope-session packages from AUR, which
|
||||||
# provide the base session framework that the Steam Deck uses.
|
# 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() {
|
setup_session_switching() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "================================================================"
|
echo "================================================================"
|
||||||
@@ -1533,6 +1655,11 @@ setup_session_switching() {
|
|||||||
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
|
info "ChimeraOS gamescope-session packages already installed (correct -git versions)"
|
||||||
fi
|
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
|
# NetworkManager Integration
|
||||||
#
|
#
|
||||||
# Omarchy uses iwd (Intel Wireless Daemon) for WiFi, but Steam requires
|
# Omarchy uses iwd (Intel Wireless Daemon) for WiFi, but Steam requires
|
||||||
@@ -1873,6 +2000,24 @@ UDISKS_POLKIT
|
|||||||
mkdir -p "$env_dir"
|
mkdir -p "$env_dir"
|
||||||
touch "$gamescope_conf"
|
touch "$gamescope_conf"
|
||||||
|
|
||||||
|
# Legacy CUSTOM_REFRESH_RATES migration (added in v0.1.9).
|
||||||
|
#
|
||||||
|
# Pre-v0.1.8 the Settings TUI wrote CUSTOM_REFRESH_RATES as a single rate
|
||||||
|
# (e.g. "165"). 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. v0.1.8+ writes a comma list (e.g. "60,165") from the TUI, but
|
||||||
|
# existing users still have the scalar in their conf. Rewrite it here so
|
||||||
|
# re-running the installer is enough to fix Gaming Mode for them — no need
|
||||||
|
# to re-open the TUI and re-pick the rate.
|
||||||
|
local legacy_rate
|
||||||
|
legacy_rate=$(grep -E '^CUSTOM_REFRESH_RATES=[0-9]+$' "$gamescope_conf" | head -1 | cut -d= -f2)
|
||||||
|
if [[ -n "$legacy_rate" && "$legacy_rate" != "60" ]]; then
|
||||||
|
info "Migrating CUSTOM_REFRESH_RATES=${legacy_rate} → 60,${legacy_rate} (v0.1.9 format)"
|
||||||
|
sed -i "s|^CUSTOM_REFRESH_RATES=.*|CUSTOM_REFRESH_RATES=60,${legacy_rate}|" "$gamescope_conf"
|
||||||
|
systemctl --user import-environment CUSTOM_REFRESH_RATES 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Per-key updater — replaces in place if present, appends if missing.
|
# Per-key updater — replaces in place if present, appends if missing.
|
||||||
# Same shape as the TUI's flush_pending so the two never fight.
|
# Same shape as the TUI's flush_pending so the two never fight.
|
||||||
set_conf_key() {
|
set_conf_key() {
|
||||||
@@ -2208,6 +2353,28 @@ notify-send -u normal -t 2000 "Gaming Mode" "Switching to Gaming Mode..." 2>/dev
|
|||||||
pkill -9 gamescope 2>/dev/null || true
|
pkill -9 gamescope 2>/dev/null || true
|
||||||
pkill -9 -f gamescope-session 2>/dev/null || true
|
pkill -9 -f gamescope-session 2>/dev/null || true
|
||||||
sleep 1
|
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
|
sudo -n chvt 2 2>/dev/null || true
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
sudo -n systemctl restart sddm
|
sudo -n systemctl restart sddm
|
||||||
@@ -2775,6 +2942,7 @@ verify_installation() {
|
|||||||
["/etc/environment.d/99-shader-cache.conf"]="644:Shader cache config"
|
["/etc/environment.d/99-shader-cache.conf"]="644:Shader cache config"
|
||||||
["/usr/local/bin/deckshift-settings"]="755:Gaming Mode settings TUI"
|
["/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:Walker 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 " FILE STATUS:"
|
||||||
echo " ------------"
|
echo " ------------"
|
||||||
|
|||||||
Reference in New Issue
Block a user