Files
I38/scripts/steam_games.py

121 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
# This file is part of I38.
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
"""
Steam game focus handler for i3.
This script listens for new windows and focuses Steam games when they appear.
It uses process tree detection to reliably identify games launched by Steam,
solving the issue where games open behind Big Picture without focus.
"""
import re
import subprocess
import time
import i3ipc
def get_window_pid(windowId):
"""Get PID from X11 window ID using xdotool."""
if not windowId:
return None
try:
result = subprocess.run(
['xdotool', 'getwindowpid', str(windowId)],
capture_output=True,
text=True,
timeout=1
)
if result.returncode == 0:
return int(result.stdout.strip())
except (subprocess.TimeoutExpired, ValueError, FileNotFoundError):
pass
return None
def is_steam_child(pid):
"""Check if PID is a descendant of Steam process."""
try:
while pid > 1:
with open(f'/proc/{pid}/comm', 'r') as f:
name = f.read().strip()
if name in ('steam', 'steamwebhelper', 'steam.sh'):
return True
with open(f'/proc/{pid}/stat', 'r') as f:
stat = f.read()
# Format: pid (comm) state ppid ...
# comm can contain spaces, so find the last ) and parse from there
closeParenIdx = stat.rfind(')')
fields = stat[closeParenIdx + 2:].split()
pid = int(fields[1]) # PPID is the 2nd field after (comm)
except (FileNotFoundError, PermissionError, ValueError, IndexError):
pass
return False
def is_steam_client(windowClass):
"""Check if window is the Steam client itself (not a game)."""
if not windowClass:
return False
# Steam client windows have class "Steam" or "steam"
return re.match(r'^steam$', windowClass, re.IGNORECASE) is not None
def on_new_window(i3, event):
"""Handle new window events - focus Steam games."""
try:
container = event.container
windowClass = container.window_class
windowId = container.window
pid = get_window_pid(windowId)
# Skip if no PID available
if not pid:
return
# Skip Steam client windows
if is_steam_client(windowClass):
return
# Check if this is a Steam game (child of Steam process)
if is_steam_child(pid):
# Spawn a tiny yad window to reset Orca's focus state
# This prevents Orca from thinking it's still in Steam
# yad will auto-close after timeout
subprocess.Popen(
['yad', '--text=', '--no-buttons', '--undecorated',
'--geometry=1x1+0+0', '--timeout=1', '--skip-taskbar'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# Wait for yad window to appear then focus it
time.sleep(0.15)
i3.command('[class="Yad"] focus')
# Let Orca register the yad window
time.sleep(0.15)
# Now focus the game window
container.command('focus')
except Exception:
# Silently ignore errors to prevent blocking i3
pass
def main():
i3 = i3ipc.Connection()
i3.on('window::new', on_new_window)
i3.main()
if __name__ == '__main__':
main()