121 lines
3.9 KiB
Python
Executable File
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()
|