fix(hypr): reliable gaming workspace focus across monitors

- Add game-focus-watcher: listens on hyprland socket, auto-focuses
  gaming workspace when a game opens, closing any special workspace
  on DP-1 first via --batch to avoid async dispatch races
- Add gaming-focus script: SUPER+G now handles special workspaces on
  DP-1 regardless of focused monitor, with toggle-back behavior
- Fix steam windowrule to send all class:steam windows to special:steam,
  not just title:Steam, preventing dialogs leaking to normal workspaces
- Fix monitor 0 -> monitor DP-1 in mkGameRules and steam_app rules
  so games always launch at correct resolution on the gaming monitor
- Extract gamingMonitor variable as single source of truth
This commit is contained in:
CLAUDE AI
2026-05-28 07:57:33 -05:00
committed by kenji
parent 4205ab5429
commit 40b7db2c00
3 changed files with 77 additions and 21 deletions
+1 -1
View File
@@ -74,7 +74,7 @@
]; ];
wayland.windowManager.hyprland.settings.exec-once = [ wayland.windowManager.hyprland.settings.exec-once = [
"[workspace special:preload silent] uwsm app -- xdg-terminal-exec"
"[workspace 1] uwsm app -- ghostty -e bash -c 'fastfetch; exec $SHELL'" # TODO: must be xdg-terminal-exec, or default user terminal "[workspace 1] uwsm app -- ghostty -e bash -c 'fastfetch; exec $SHELL'" # TODO: must be xdg-terminal-exec, or default user terminal
"[workspace special:preload silent] uwsm app -- xdg-terminal-exec"
]; ];
} }
+15 -12
View File
@@ -3,20 +3,23 @@
#!/usr/bin/env bash #!/usr/bin/env bash
target_workspace="$1" target_workspace="$1"
# Get current workspace info # activeworkspace always returns the underlying workspace, even when a special
current_info=$(${pkgs.hyprland}/bin/hyprctl activeworkspace -j) # workspace is open. Check the monitor's specialWorkspace field instead.
current=$(echo "$current_info" | ${pkgs.jq}/bin/jq -r '.id') special=$(${pkgs.hyprland}/bin/hyprctl monitors -j | ${pkgs.jq}/bin/jq -r '.[] | select(.focused) | .specialWorkspace.name')
# Check if we're in a special workspace (negative ID) if [[ -n "$special" ]]; then
if [[ $current -lt 0 ]]; then ${pkgs.hyprland}/bin/hyprctl dispatch togglespecialworkspace "''${special#special:}"
# We're in a special workspace, force switch to target workspace current=$(${pkgs.hyprland}/bin/hyprctl activeworkspace -j | ${pkgs.jq}/bin/jq -r '.id')
${pkgs.hyprland}/bin/hyprctl dispatch focusworkspaceoncurrentmonitor "$target_workspace" if [[ $current -ne $target_workspace ]]; then
elif [[ $current -eq $target_workspace ]]; then ${pkgs.hyprland}/bin/hyprctl dispatch focusworkspaceoncurrentmonitor "$target_workspace"
# We're already on the target workspace, toggle back to previous fi
${pkgs.hyprland}/bin/hyprctl dispatch workspace previous
else else
# We're on a different normal workspace, switch to target using split:workspace current=$(${pkgs.hyprland}/bin/hyprctl activeworkspace -j | ${pkgs.jq}/bin/jq -r '.id')
${pkgs.hyprland}/bin/hyprctl dispatch split:workspace "$target_workspace" if [[ $current -eq $target_workspace ]]; then
${pkgs.hyprland}/bin/hyprctl dispatch workspace previous
else
${pkgs.hyprland}/bin/hyprctl dispatch split:workspace "$target_workspace"
fi
fi fi
''; '';
in { in {
+61 -8
View File
@@ -4,15 +4,62 @@
myConfig, myConfig,
... ...
}: let }: 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
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch togglespecialworkspace ''${special#special:};dispatch focusmonitor ${gamingMonitor};dispatch workspace name:gaming"
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
${pkgs.hyprland}/bin/hyprctl --batch "dispatch focusmonitor ${gamingMonitor};dispatch togglespecialworkspace ''${special#special:};dispatch focusmonitor ${gamingMonitor};dispatch workspace name:gaming"
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) # Games that should have `stayfocused` applied (to avoid multi-monitor focus issues)
stayFocusedGames = [ stayFocusedGames = [
"Deadlock" # "Deadlock"
"project8" # "project8"
"citadel" # "citadel"
]; ];
mkGameRules = selector: [ mkGameRules = selector: [
"monitor 0, ${selector}" "monitor ${gamingMonitor}, ${selector}"
"fullscreen, ${selector}" "fullscreen, ${selector}"
"immediate, ${selector}" "immediate, ${selector}"
"tile, ${selector}" "tile, ${selector}"
@@ -30,6 +77,8 @@ in {
protonup-qt protonup-qt
protontricks protontricks
mangohud mangohud
gaming-focus
game-focus-watcher
# via # via
]; ];
@@ -47,17 +96,18 @@ in {
wayland.windowManager.hyprland.settings = { wayland.windowManager.hyprland.settings = {
workspace = [ workspace = [
"name:gaming, monitor:0, default:true" "name:gaming, monitor:${gamingMonitor}, default:true"
]; ];
exec-once = [ exec-once = [
"[workspace special:steam silent] uwsm app -- steam" "[workspace special:steam silent] uwsm app -- steam"
"game-focus-watcher"
]; ];
bindd = [ bindd = [
"SUPER, A, Toggle Steam, togglespecialworkspace, steam" "SUPER, A, Toggle Steam, togglespecialworkspace, steam"
"SUPER SHIFT, A, Move to Steam Special Workspace, movetoworkspace, special:steam" "SUPER SHIFT, A, Move to Steam Special Workspace, movetoworkspace, special:steam"
"SUPER, G, Switch to Gaming Workspace, workspace, name:gaming" "SUPER, G, Switch to Gaming Workspace, exec, gaming-focus"
"SUPER SHIFT, G, Move to Gaming Workspace, movetoworkspace, name:gaming" "SUPER SHIFT, G, Move to Gaming Workspace, movetoworkspace, name:gaming"
]; ];
@@ -71,16 +121,19 @@ in {
"float, class:^(steam)$" "float, class:^(steam)$"
# Suppress focus stealing from dialogs/etc. # Suppress focus stealing from dialogs/etc.
"noinitialfocus, class:^(steam)$" "noinitialfocus, class:^(steam)$"
"suppressevent activate, class:^(steam)$" "suppressevent activate fullscreen maximize, class:^(steam)$"
# --- STEAM CLIENT OVERRIDE --- # --- STEAM CLIENT OVERRIDE ---
# Override the float for the main Steam client, tile it, and move it to the special workspace. # Override the float for the main Steam client, tile it, and move it to the special workspace.
"tile, class:^(steam)$, title:^(Steam)$" "tile, class:^(steam)$, title:^(Steam)$"
"workspace special:steam, 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 --- # --- STEAM GAME OVERRIDES ---
# Override the float for actual games and move them to the gaming workspace. # Override the float for actual games and move them to the gaming workspace.
# 1. Auto-detected steam_app games (like Deadlock). # 1. Auto-detected steam_app games (like Deadlock).
"monitor ${gamingMonitor}, class:^(steam_app_\\d+)$"
"tile, class:^(steam_app_\\d+)$" "tile, class:^(steam_app_\\d+)$"
"fullscreen, class:^(steam_app_\\d+)$" "fullscreen, class:^(steam_app_\\d+)$"
"immediate, class:^(steam_app_\\d+)$" "immediate, class:^(steam_app_\\d+)$"