diff --git a/i38.sh b/i38.sh index 8f34669..87162dc 100755 --- a/i38.sh +++ b/i38.sh @@ -314,6 +314,9 @@ screenlockPinHash="$screenlockPinHash" # Personal mode personalModeEnabled="${personalModeEnabled:-1}" personalModeKey="$personalModeKey" + +# WayTray configuration (0=use I38 config, 1=keep user config) +waytrayUseI38Config="${waytrayUseI38Config:-0}" EOF dialog --title "I38" --msgbox "Configuration saved to $configFile\n\nYou can edit this file manually or delete it to reconfigure from scratch." 0 0 @@ -431,9 +434,16 @@ write_waytray_config() { mkdir -p "${waytrayConfigDir}" - # Ask user if config already exists + # Ask user if config already exists (unless preference already saved) if [[ -f "${waytrayConfig}" ]]; then - if ! yesno "Existing waytray configuration detected. Replace with I38's minimal tray-only config?\n\n(Select 'No' to keep your existing waytray configuration with all modules enabled)"; then + if [[ -z "$waytrayUseI38Config" ]]; then + if yesno "Existing waytray configuration detected. Replace with I38's minimal tray-only config?\n\n(Select 'No' to keep your existing waytray configuration with all modules enabled)"; then + waytrayUseI38Config=0 + else + waytrayUseI38Config=1 + fi + fi + if [[ $waytrayUseI38Config -ne 0 ]]; then return 0 # User wants to keep existing config fi fi @@ -1141,6 +1151,10 @@ $(if [[ $sounds -eq 0 ]]; then echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py" fi fi +# Steam game focus handler (i3 only) - focuses games when they open behind Big Picture +if [[ $usingSway -ne 0 ]] && command -v steam &> /dev/null; then + echo "exec --no-startup-id ${i3Path}/scripts/steam_games.py" +fi # i3 watchdog - monitors i3 responsiveness and auto-recovers from freezes if [[ $usingSway -ne 0 ]]; then echo "exec_always --no-startup-id ${i3Path}/scripts/i3_watchdog.sh" diff --git a/scripts/steam_games.py b/scripts/steam_games.py new file mode 100755 index 0000000..0b3cb5c --- /dev/null +++ b/scripts/steam_games.py @@ -0,0 +1,120 @@ +#!/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 . + +""" +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()