Files
nixos/modules/home/gaming.nix
T
CLAUDE AI fe0d006f2e fix(hypr): reliable gaming workspace focus across monitors
- Add gaming-focus script: closes special workspace on DP-1 via
  --batch dispatch (fixes async race), then focuses gaming workspace;
  replaces raw workspace binding for SUPER+G
- Add game-focus-watcher: listens on Hyprland socket, auto-focuses
  gaming workspace when a game launches regardless of current monitor
- Fix monitor 0 → DP-1 in mkGameRules and steam_app windowrules
- Fix steam dialogs leaking to normal workspaces (broaden workspace
  rule from title:Steam to all class:steam)
- Extract gamingMonitor variable as single source of truth
- Drop redundant workspace dispatch after togglespecialworkspace to
  prevent game freeze on special→gaming transition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 08:13:47 -05:00

165 lines
5.8 KiB
Nix

{
pkgs,
lib,
myConfig,
...
}: let
gamingMonitor = "DP-1";
gaming-focus = pkgs.writeShellScriptBin "gaming-focus" ''
# If already on gaming workspace on the gaming monitor, go back
current=$(${pkgs.hyprland}/bin/hyprctl activeworkspace -j | ${pkgs.jq}/bin/jq -r '.name')
focused=$(${pkgs.hyprland}/bin/hyprctl monitors -j | ${pkgs.jq}/bin/jq -r '.[] | select(.focused) | .name')
if [[ "$current" == "gaming" && "$focused" == "${gamingMonitor}" ]]; then
${pkgs.hyprland}/bin/hyprctl dispatch workspace previous
exit 0
fi
# Close special workspace on gaming monitor if open
special=$(${pkgs.hyprland}/bin/hyprctl monitors -j | ${pkgs.jq}/bin/jq -r '.[] | select(.name == "${gamingMonitor}") | .specialWorkspace.name')
if [[ -n "$special" ]]; then
# Gaming workspace is already underneath just close the special overlay
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch togglespecialworkspace ''${special#special:}"
else
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch workspace name:gaming"
fi
'';
game-focus-watcher = pkgs.writeShellScriptBin "game-focus-watcher" ''
handle() {
case $1 in
openwindow*)
data="''${1#openwindow>>}"
class=$(echo "$data" | cut -d',' -f3)
if [[ "$class" =~ ^steam_app_[0-9]+ ]] || \
[[ "$class" == "gamescope" ]] || \
[[ "$class" =~ ^wine- ]] || \
[[ "$class" == "lutris" ]] || \
[[ "$class" == "heroic" ]]; then
special=$(${pkgs.hyprland}/bin/hyprctl monitors -j | ${pkgs.jq}/bin/jq -r '.[] | select(.name == "${gamingMonitor}") | .specialWorkspace.name')
if [[ -n "$special" ]]; then
# Gaming workspace is already underneath just close the special overlay
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch togglespecialworkspace ''${special#special:}"
else
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch workspace name:gaming"
fi
fi
;;
esac
}
${pkgs.socat}/bin/socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" | while read -r line; do
handle "$line"
done
'';
# Games that should have `stayfocused` applied (to avoid multi-monitor focus issues)
stayFocusedGames = [
# "Deadlock"
# "project8"
# "citadel"
];
mkGameRules = selector: [
"monitor ${gamingMonitor}, ${selector}"
"fullscreen, ${selector}"
"immediate, ${selector}"
"tile, ${selector}"
];
stayFocusedRules = lib.flatten (map (
game: [
"stayfocused, title:^(${game})$"
"stayfocused, class:^(${game})$"
]
)
stayFocusedGames);
in {
home.packages = with pkgs; [
protonup-qt
protontricks
mangohud
gaming-focus
game-focus-watcher
# via
];
programs.mangohud = {
enable = true;
settings = {
full = true;
no_display = true; # Don't show by default (toggle with Shift+F12)
cpu_temp = true;
gpu_temp = true;
ram = true;
vram = true;
};
};
wayland.windowManager.hyprland.settings = {
workspace = [
"name:gaming, monitor:${gamingMonitor}, default:true"
];
exec-once = [
"[workspace special:steam silent] uwsm app -- steam"
"game-focus-watcher"
];
bindd = [
"SUPER, A, Toggle Steam, togglespecialworkspace, steam"
"SUPER SHIFT, A, Move to Steam Special Workspace, movetoworkspace, special:steam"
"SUPER, G, Switch to Gaming Workspace, exec, gaming-focus"
"SUPER SHIFT, G, Move to Gaming Workspace, movetoworkspace, name:gaming"
];
windowrulev2 =
[
"plugin:hyprbars:nobar, class:^(steam)$"
"plugin:hyprbars:nobar, class:^(steam_app_\\d+)$"
# --- STEAM GENERAL RULES ---
# Default ALL steam class windows to float. This catches all dialogs & properties windows.
"float, class:^(steam)$"
# Suppress focus stealing from dialogs/etc.
"noinitialfocus, class:^(steam)$"
"suppressevent activate fullscreen maximize, class:^(steam)$"
# --- STEAM CLIENT OVERRIDE ---
# Override the float for the main Steam client, tile it, and move it to the special workspace.
"tile, class:^(steam)$, title:^(Steam)$"
# All steam class windows go to special:steam (dialogs, store, friends, etc.)
# Game overrides below take precedence for actual games.
"workspace special:steam, class:^(steam)$"
# --- STEAM GAME OVERRIDES ---
# Override the float for actual games and move them to the gaming workspace.
# 1. Auto-detected steam_app games (like Deadlock).
"monitor ${gamingMonitor}, class:^(steam_app_\\d+)$"
"tile, class:^(steam_app_\\d+)$"
"fullscreen, class:^(steam_app_\\d+)$"
"immediate, class:^(steam_app_\\d+)$"
"workspace name:gaming, class:^(steam_app_\\d+)$"
# 2. Behavior-detected manual games (e.g. ARC Raiders).
# This moves any steam game to the gaming workspace and tiles it when it becomes fullscreen.
"tile, class:^(steam)$, fullscreen:1"
"workspace name:gaming, class:^(steam)$, fullscreen:1"
]
# Other auto-detected non-steam games
++ (mkGameRules "class:^(gamescope)$")
++ (mkGameRules "class:^(lutris)$")
++ (mkGameRules "class:^(heroic)$")
++ (mkGameRules "class:^wine-.*$")
++ (mkGameRules "title:^Wine .*$")
++ (mkGameRules "initialTitle:^(?i)godot.*$")
# ++ [
# "monitor 0, initialTitle:^(?i)godot.*$"
# "fullscreen, initialTitle:^(?i)godot.*$"
# "tile, initialTitle:^(?i)godot.*$"
# ]
# Stayfocused rules
++ stayFocusedRules;
};
}