From d91735a2fa2b10941db519a8ca38ee84f4a35285 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Sat, 18 Apr 2026 17:10:23 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Wrapper,=20Logging=20und=20Doku=20?= =?UTF-8?q?=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Debug-Logging optional gemacht, den Legacy-Wrapper portabler aufgebaut (relative Pfade, Debug-Weitergabe, Python-Fallback) und die README für Endanwender und Entwickler klarer neu strukturiert. --- CLAUDE.md | 62 ++++++++++++++---- README.md | 161 +++++++++++++++++++++++++++++++++-------------- focusfix.py | 56 ++++++++++------- launch.vbs | 55 ++++++++++++---- requirements.txt | 2 + 5 files changed, 242 insertions(+), 94 deletions(-) create mode 100644 requirements.txt diff --git a/CLAUDE.md b/CLAUDE.md index 99c5488..1b78877 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,42 +8,78 @@ FocusFix is a Windows utility that forces a game window into the foreground afte ## Architecture -- **`focusfix.py`** — Python script that supports two modes: `--process Name` for monitoring an already-known process, or direct Steam wrapper mode where `%command%` is passed as positional arguments. In wrapper mode it derives the process name from the target executable, launches the game itself, then polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Runtime logging goes to `focusfix.log`. -- **`launch.vbs`** — Legacy Steam wrapper. No longer required for normal usage, but still present in the repository. +- **`focusfix.py`** — Python script that supports two modes: `--process Name` for monitoring an already-known process, or direct Steam wrapper mode where `%command%` is passed as positional arguments. In wrapper mode it derives the process name from the target executable, launches the game itself, then polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Debug logging is only enabled explicitly with `--debug`/`-debug`, writes to `focusfix.log`, and overwrites the file on each new script start. +- **`launch.vbs`** — Legacy Steam wrapper. Still supported as a fallback, but not the primary user path anymore. It resolves `focusfix.py` relative to its own location, prefers a sibling `.venv\Scripts\pythonw.exe`, falls back to `pythonw.exe` from `PATH`, and forwards `-d`/`-debug`/`--debug` to `focusfix.py` without passing those flags to the game. +- **`requirements.txt`** — Source-install dependencies for `focusfix.py` (`psutil`, `pywin32`). ## Setup +``` +py -m pip install -r requirements.txt +``` + +Optional for development: + ``` py -m venv .venv .venv\Scripts\activate -pip install psutil pywin32 +pip install -r requirements.txt ``` ## Steam Launch Options +Preferred for end users: + ``` -"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\focusfix.py" %command% +"C:\Path\To\FocusFix\FocusFix.exe" %command% ``` -Same launch option works for all Steam games with similar foreground/focus issues — the process name is extracted automatically. +Direct Python/script usage: + +``` +"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command% +``` + +Legacy wrapper fallback: + +``` +wscript "C:\Path\To\FocusFix\launch.vbs" %command% +wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command% +``` + +All Steam variants above work generically across games with similar foreground/focus issues because the process name is derived from the target executable. ## Sunshine / Moonlight -Use as command preparation in Sunshine (full venv path required): +Preferred for end users: ``` -"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\focusfix.py" --process "ProcessName" +"C:\Path\To\FocusFix\FocusFix.exe" --process "ProcessName" +``` + +Direct Python/script usage: + +``` +"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "ProcessName" ``` ## Running Manually (for testing) ``` -.venv\Scripts\python focusfix.py --process CrimsonDesert -.venv\Scripts\python focusfix.py --process CrimsonDesert --timeout 180 -.venv\Scripts\python focusfix.py "C:\Games\CrimsonDesert.exe" +python focusfix.py --process CrimsonDesert +python focusfix.py --process CrimsonDesert --timeout 180 +python focusfix.py "C:\Games\CrimsonDesert.exe" +python focusfix.py -debug "C:\Games\CrimsonDesert.exe" ``` -Use either `--process` or a game command. Runtime logging goes to `focusfix.log`. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console. +Use either `--process` or a game command. By default no log file is created. When started with `--debug` or `-debug`, runtime logging goes to `focusfix.log` and overwrites any existing file from a previous run. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console. + +## Working Assumptions + +- End-user docs and defaults should prefer `FocusFix.exe` over Python or `.venv`-based usage. +- Source-based usage is still supported for developers and power users through `requirements.txt`. +- `launch.vbs` is legacy/fallback behavior, not the primary integration path. +- Do not assume `.venv` exists; wrapper behavior explicitly supports a fallback to global `pythonw.exe`. ## Key Constants in focusfix.py @@ -54,6 +90,6 @@ Use either `--process` or a game command. Runtime logging goes to `focusfix.log` - `SetForegroundWindow` is restricted by Windows unless the calling process owns the foreground. Workaround: simulate an `Alt` keypress + `AttachThreadInput` to the foreground thread. - Alt key release is in a `finally` block to prevent it from staying pressed on errors. -- `pythonw.exe` (not `python.exe`) is required for invisible background execution. -- `launch.vbs` is legacy-only; normal Steam usage now calls `focusfix.py` directly. +- `pythonw.exe` (not `python.exe`) is required for invisible background execution when using the Python/script path. +- Normal user-facing guidance should point to `FocusFix.exe`; direct `focusfix.py` usage is the source/developer path. - Window selection prefers titled windows over titleless ones (prevents overlay issues like in Wuthering Waves). diff --git a/README.md b/README.md index dde0792..9df2a90 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,15 @@ Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bring ## Inhaltsverzeichnis +- [Schnellstart](#schnellstart) + - [Für Nutzer](#fur-nutzer) + - [Für Entwickler](#fur-entwickler) - [Problembeschreibung](#problembeschreibung) - [Lösungsansatz](#lösungsansatz) - [Architektur](#architektur) - [Voraussetzungen](#voraussetzungen) -- [Installation](#installation) +- [Direkte Verwendung](#direkte-verwendung) +- [Aus Quellcode ausführen](#aus-quellcode-ausfuhren) - [Konfiguration](#konfiguration) - [Steam](#steam) - [Sunshine / Moonlight](#sunshine--moonlight) @@ -29,6 +33,47 @@ Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bring --- +## Schnellstart + +### Für Nutzer + +Wenn du FocusFix direkt verwenden willst, ist eine fertige `FocusFix.exe` der empfohlene Weg. Du brauchst dann weder ein Virtual Environment noch ein manuelles `pip install`. + +Steam-Beispiel: + +```text +"C:\Path\To\FocusFix\FocusFix.exe" %command% +``` + +Mit Debug-Logging: + +```text +"C:\Path\To\FocusFix\FocusFix.exe" -debug %command% +``` + +### Für Entwickler + +Wenn du FocusFix aus dem Repository starten oder weiterentwickeln willst: + +```bash +cd C:\Path\To\FocusFix +py -m pip install -r requirements.txt +``` + +Danach kannst du `focusfix.py` direkt mit deinem Python-Interpreter oder über `pythonw.exe` verwenden. + +Steam-Beispiel für die Quellcode-Nutzung mit lokalem `.venv`: + +```text +"C:\Projekte\FocusFix\.venv\Scripts\pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command% +``` + +Ohne lokales `.venv` und mit globalem Python im `PATH`: + +```text +"pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command% +``` + ## Problembeschreibung Beim Start eines Spiels über Steam oder Sunshine/Moonlight öffnet sich das Spielfenster im **Hintergrund**. Die Steam-Oberfläche, die Windows-Taskleiste oder andere Fenster verdecken es, und der Spieler muss manuell per Alt+Tab wechseln. @@ -86,33 +131,31 @@ Steam Launch Options / Sunshine Befehlsvorbereitung | Komponente | Version | Hinweis | |---|---|---| | Windows | 10 / 11 | Benötigt Win32 API (user32.dll) | -| Python | 3.10+ | `py` Launcher muss verfügbar sein | -| pip-Pakete | `psutil`, `pywin32` | Werden im `.venv` installiert | | Steam | Beliebig | Startoptionen müssen konfigurierbar sein | +| Python | 3.10+ | Nur für Quellcode-Nutzung erforderlich | +| pip-Pakete | `psutil`, `pywin32` | Nur für Quellcode-Nutzung erforderlich | -## Installation +## Direkte Verwendung + +Für normale Nutzer ist eine fertige `.exe` der einfachste Weg. + +## Aus Quellcode ausführen ```bash cd C:\Path\To\FocusFix - -# Virtual Environment erstellen -py -m venv .venv - -# Aktivieren -.venv\Scripts\activate - -# Abhängigkeiten installieren -pip install psutil pywin32 +py -m pip install -r requirements.txt ``` -### Dateistruktur nach Installation +Optional kannst du dafür auch ein Virtual Environment verwenden. Für Entwickler ist das sinnvoll, für Endanwender aber nicht der Standardweg. + +### Dateistruktur im Repository ``` C:\Path\To\FocusFix\ -├── .venv\ # Python Virtual Environment ├── focusfix.py # Hauptscript -├── focusfix.log # Logdatei (wird automatisch erstellt) +├── focusfix.log # Logdatei (nur bei --debug / -debug) ├── launch.vbs # Legacy-Wrapper, optional +├── requirements.txt # Python-Abhängigkeiten für Quellcode-Nutzung ├── CLAUDE.md # Kontext für Claude Code └── README.md # Diese Dokumentation ``` @@ -125,10 +168,25 @@ C:\Path\To\FocusFix\ 2. Rechtsklick auf das Spiel → **Eigenschaften** 3. Unter **Allgemein → Startoptionen** eine der folgenden Varianten eintragen: -#### Variante A — Empfohlen: direkt mit `focusfix.py` +#### Variante A — Empfohlen für Nutzer: `FocusFix.exe` ```text -"C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command% +"C:\Path\To\FocusFix\FocusFix.exe" %command% +``` + +Mit Debug-Logging: + +```text +"C:\Path\To\FocusFix\FocusFix.exe" -debug %command% +``` + +Die `.exe` übernimmt denselben Wrapper-Modus wie `focusfix.py`: Sie erhält `%command%`, startet das Spiel und überwacht danach den Fokus. Diese Variante ist für Endanwender gedacht. +Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden. + +#### Variante B — Quellcode direkt mit `focusfix.py` + +```text +"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command% ``` `focusfix.py` extrahiert den Prozessnamen automatisch aus `%command%`, startet das eigentliche Spiel und überwacht danach den Fensterfokus. Die gleiche Startoption kann für beliebige Steam-Spiele mit vergleichbarem Fokus-Problem verwendet werden. @@ -139,41 +197,49 @@ Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen I - `"...\focusfix.py"` — Python-Script im Steam-Wrapper-Modus - `%command%` — Steam-Platzhalter, wird zur Laufzeit durch den vollständigen Pfad zur Spiel-Executable ersetzt, z.B. `"C:\SteamLibrary\...\Game.exe"` -#### Variante B — Legacy: über `launch.vbs` +#### Variante C — Legacy: über `launch.vbs` Wenn du den alten VBScript-Wrapper weiterhin verwenden willst, kannst du in Steam Folgendes eintragen: ```text wscript "C:\Path\To\FocusFix\launch.vbs" %command% +Beispiel: "C:\Projekte\FocusFix\launch.vbs" %command% ``` -`launch.vbs` extrahiert den Prozessnamen aus `%command%`, startet `focusfix.py` unsichtbar mit `--process` und startet danach das eigentliche Spiel. Diese Variante funktioniert weiterhin, ist aber nur noch als Legacy-/Fallback-Weg gedacht, weil `focusfix.py` den Wrapper-Job inzwischen selbst übernehmen kann. -Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden. +Mit Debug-Logging: + +```text +wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command% +wscript "C:\Projekte\FocusFix\launch.vbs" -debug %command% +``` + +`launch.vbs` extrahiert den Prozessnamen aus `%command%`, startet `focusfix.py` unsichtbar mit `--process` und startet danach das eigentliche Spiel. `-d`, `-debug` und `--debug` werden an `focusfix.py` weitergereicht und nicht an das Spiel übergeben. Diese Variante funktioniert weiterhin, ist aber nur noch als Legacy-/Fallback-Weg gedacht, weil `focusfix.py` den Wrapper-Job inzwischen selbst übernehmen kann. +Der Wrapper erwartet `focusfix.py` relativ zu seinem eigenen Speicherort. Für Python nutzt er zuerst `.venv\Scripts\pythonw.exe` im selben Ordner und fällt, falls nicht vorhanden, auf `pythonw.exe` aus der globalen Installation beziehungsweise dem `PATH` zurück. **Erklärung:** - `wscript` — startet VBScript ohne sichtbares Konsolenfenster - `"...\launch.vbs"` — Legacy-Wrapper, der `focusfix.py` indirekt startet +- `-debug` — optional, aktiviert `focusfix.log` für `focusfix.py` - `%command%` — Steam-Platzhalter mit dem ursprünglichen Spielstartbefehl -#### Variante C — Release-Exe - -Wenn du eine gebaute Release-Datei verwenden willst, kannst du statt Python direkt die `.exe` in Steam eintragen. In diesem Repository liegt aktuell eine Build-Datei unter `dist\nuinstall\FocusFix.exe`: - -```text -"C:\Path\To\FocusFix\dist\nuinstall\FocusFix.exe" %command% -``` - -Die `.exe` übernimmt denselben Wrapper-Modus wie `focusfix.py`: Sie erhält `%command%`, startet das Spiel und überwacht danach den Fokus. Diese Variante ist praktisch, wenn du keine sichtbare Python-Installation oder kein venv in den Steam-Startoptionen referenzieren willst. - -**Wichtig:** Der Dateiname der Release-Exe hängt vom Build ab. Wenn du später eine neu benannte Exe wie `FocusFix.exe` baust, muss genau dieser Pfad in Steam eingetragen werden. -Auch hier sind die Pfade im Beispiel Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden. - ### Sunshine / Moonlight -In Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel eintragen: +Für Nutzer mit `.exe`: ```text -"C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "Prozessname" +"C:\Path\To\FocusFix\FocusFix.exe" --process "Prozessname" +``` + +Mit Debug-Logging: + +```text +"C:\Path\To\FocusFix\FocusFix.exe" --debug --process "Prozessname" +``` + +Für Quellcode-Nutzung in Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel: + +```text +"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "Prozessname" ``` Den Prozessnamen im Task-Manager ablesen, ohne `.exe`. Beispiele: @@ -183,7 +249,7 @@ Den Prozessnamen im Task-Manager ablesen, ohne `.exe`. Beispiele: | Crimson Desert | `CrimsonDesert` | | Wuthering Waves | `Client-Win64-Shipping` | -**Wichtig:** Der vollständige Pfad zu `pythonw.exe` im `.venv` ist nötig, damit die Abhängigkeiten (`psutil`, `pywin32`) gefunden werden. `py` oder `python` nutzt sonst möglicherweise eine andere Python-Installation. +**Wichtig:** Wenn du den Quellcode-Weg nutzt, muss der verwendete Python-Interpreter Zugriff auf die installierten Abhängigkeiten aus `requirements.txt` haben. `pythonw.exe` ist für unsichtbare Hintergrundausführung sinnvoller als `python.exe`. ## Ablauf zur Laufzeit @@ -245,7 +311,7 @@ t=~15s Erfolg → focusfix.py beendet sich ### launch.vbs -`launch.vbs` ist nur noch ein Legacy-Wrapper. Die aktuelle empfohlene Steam-Konfiguration nutzt `focusfix.py` direkt. Die Datei kann als Fallback im Repository bleiben, wird für den normalen Ablauf aber nicht mehr benötigt. +`launch.vbs` ist nur noch ein Legacy-Wrapper. Die aktuelle empfohlene Nutzung für Endanwender erfolgt über `FocusFix.exe`. Die Datei kann als Fallback im Repository bleiben, wird für den normalen Ablauf aber nicht mehr benötigt. Der Wrapper löst `focusfix.py` relativ zu seinem eigenen Ordner auf, verwendet bevorzugt `.venv\Scripts\pythonw.exe` daneben und fällt sonst auf `pythonw.exe` aus der globalen Python-Installation beziehungsweise dem `PATH` zurück. `-d`/`-debug`/`--debug` kann er an `focusfix.py` weitergeben. ## Konfigurationsparameter @@ -271,7 +337,8 @@ Es muss entweder `--process` oder ein Spielbefehl übergeben werden. | Pfad | Zweck | |---|---| -| `C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe` | Beispielpfad zum Python-Interpreter ohne Konsolenfenster, für Steam empfohlen | +| `C:\Path\To\FocusFix\FocusFix.exe` | Empfohlener Pfad für Endanwender | +| `C:\Path\To\Python\pythonw.exe` | Beispielpfad zum Python-Interpreter ohne Konsolenfenster, für Quellcode-Nutzung | | `C:\Path\To\FocusFix\focusfix.py` | Beispielpfad zum Hauptscript | Bei einer Änderung des Installationsverzeichnisses müssen diese Pfade in den Steam-Startoptionen oder in Sunshine angepasst werden. @@ -312,27 +379,27 @@ Das wichtigste Unterscheidungsmerkmal ist hier die Fenstergröße. Kleine Startf ```bash cd C:\Path\To\FocusFix -.venv\Scripts\python focusfix.py --process "CrimsonDesert" --timeout 5 +python focusfix.py --process "CrimsonDesert" --timeout 5 ``` ### Steam-Wrapper-Modus testen ```bash -.venv\Scripts\python focusfix.py "C:\Path\To\Game\TestGame.exe" +python focusfix.py "C:\Path\To\Game\TestGame.exe" ``` -Die Laufzeit-Logs landen in `focusfix.log`. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe. Bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen. +Standardmäßig erzeugt FocusFix keine Logdatei. Mit `--debug` oder `-debug` landen die Laufzeit-Logs in `focusfix.log`; bei jedem neuen Scriptstart wird die Datei dabei überschrieben. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe. Bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen. ### Gesamtablauf testen 1. Steam-Startoptionen wie oben konfigurieren. 2. Spiel über Steam starten. 3. Erwartung: Spielfenster erscheint nach dem Ladescreen im Vordergrund, ohne zusätzliches Konsolenfenster. -4. `focusfix.log` prüfen. +4. Falls mit `--debug` gestartet wurde, `focusfix.log` prüfen. ## Debugging -Alle Informationen werden in `focusfix.log` protokolliert: +Für die Fehlersuche FocusFix mit `--debug` oder `-debug` starten. Dann werden alle Informationen in `focusfix.log` protokolliert; ein vorhandenes Logfile aus einem früheren Lauf wird dabei ersetzt: - Gefundene Kandidaten-Fenster mit Handle, Titel, Größe und PID - Fensterwechsel und ignorierte titellose Fenster @@ -343,12 +410,12 @@ Alle Informationen werden in `focusfix.log` protokolliert: | Symptom | Ursache | Lösung | |---|---|---| -| Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht und `pythonw.exe`/`focusfix.py` korrekt referenziert werden | -| `ModuleNotFoundError: psutil` | Falsche Python-Installation | Vollständigen Pfad zu `.venv\Scripts\pythonw.exe` verwenden | +| Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht und `.exe` oder `pythonw.exe`/`focusfix.py` korrekt referenziert werden | +| `ModuleNotFoundError: psutil` | Falsche Python-Installation | `py -m pip install -r requirements.txt` ausführen oder den richtigen Python-Interpreter verwenden | | Script findet Prozess nicht | Prozessname stimmt nicht oder Launcher-Kette ist ungewöhnlich | Mit `--process` testen oder Log prüfen | | Fenster wird nicht fokussiert | Windows-Restriktion zu streng | Retry-Anzahl/Delay in Konstanten erhöhen | | Alt-Taste bleibt hängen | Spiel mit Admin-Rechten, Script ohne | Script ebenfalls als Admin starten | -| CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Steam-/Sunshine-Konfiguration prüfen | +| CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Steam-/Sunshine-Konfiguration prüfen oder `.exe` verwenden | | Verdunkeltes/blockiertes Spiel | Titelloses Overlay-Fenster | Log prüfen; sollte meist durch Fenster-mit-Titel-Bevorzugung abgefangen werden | ### Fenster-Inspektion per PowerShell diff --git a/focusfix.py b/focusfix.py index 3e875e8..cb4ebbc 100644 --- a/focusfix.py +++ b/focusfix.py @@ -26,11 +26,9 @@ import win32process from pathlib import Path _LOG_PATH = Path(__file__).resolve().parent / "focusfix.log" -logging.basicConfig( - filename=str(_LOG_PATH), - level=logging.DEBUG, - format="%(asctime)s %(levelname)s %(message)s", -) +_LOGGER = logging.getLogger("focusfix") +_LOGGER.addHandler(logging.NullHandler()) +_LOGGER.propagate = False # Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen. MIN_WINDOW_WIDTH = 800 @@ -46,8 +44,21 @@ def derive_process_name(command: list[str]) -> str: return Path(command[0]).stem +def configure_logging(enabled: bool, log_path: Path = _LOG_PATH) -> None: + _LOGGER.handlers.clear() + if not enabled: + _LOGGER.addHandler(logging.NullHandler()) + _LOGGER.setLevel(logging.CRITICAL + 1) + return + + file_handler = logging.FileHandler(log_path, mode="w", encoding="utf-8") + file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + _LOGGER.addHandler(file_handler) + _LOGGER.setLevel(logging.DEBUG) + + def launch_game(command: list[str]) -> subprocess.Popen: - logging.info("Launching game command: %r", command) + _LOGGER.info("Launching game command: %r", command) return subprocess.Popen(command) @@ -82,7 +93,7 @@ def find_game_window(pids: set[int]) -> int | None: height = rect[3] - rect[1] if width >= MIN_WINDOW_WIDTH: title = win32gui.GetWindowText(hwnd) - logging.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid) + _LOGGER.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid) found.append((hwnd, title, width, height)) return True @@ -115,7 +126,7 @@ def wait_for_stable_size(hwnd: int, polls: int = 3, interval: float = 0.5, stable_count = 0 last_size = size time.sleep(interval) - logging.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)", + _LOGGER.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)", hwnd, last_size[0], last_size[1], stable_count) return last_size @@ -154,11 +165,11 @@ def bring_to_front(hwnd: int) -> bool: ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False) result = is_foreground(hwnd) - logging.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)", - result, win32gui.GetForegroundWindow(), hwnd) + _LOGGER.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)", + result, win32gui.GetForegroundWindow(), hwnd) return result except Exception as e: - logging.exception("bring_to_front failed: %s", e) + _LOGGER.exception("bring_to_front failed: %s", e) return False finally: win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0) @@ -167,12 +178,15 @@ def bring_to_front(hwnd: int) -> bool: def main() -> None: parser = argparse.ArgumentParser() + parser.add_argument("-d", "--debug", "-debug", action="store_true", + help="Debug-Logging nach focusfix.log aktivieren") parser.add_argument("--timeout", type=int, default=120) parser.add_argument("--process", type=str, help="Prozessname des Spiels (ohne .exe)") parser.add_argument("command", nargs="*", help="Steam-Spielbefehl, z.B. %%command%%") args, extra_command_args = parser.parse_known_args() + configure_logging(args.debug) process_name = args.process command = args.command + extra_command_args @@ -187,13 +201,13 @@ def main() -> None: try: launch_game(command) except OSError as exc: - logging.exception("Failed to launch game command: %s", exc) + _LOGGER.exception("Failed to launch game command: %s", exc) sys.exit(1) elif not process_name: parser.error("Either --process or a game command is required.") deadline = time.time() + args.timeout - logging.info("Monitoring process name: %s", process_name) + _LOGGER.info("Monitoring process name: %s", process_name) # --- Phase 1: Warten auf den Prozess --- pids: set[int] = set() @@ -211,13 +225,13 @@ def main() -> None: pids = find_process_pids(process_name) hwnd = find_game_window(pids) if hwnd: - logging.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd)) + _LOGGER.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd)) # Warten bis das Spiel sein Fenster vollständig aufgebaut hat time.sleep(3) break time.sleep(0.5) else: - logging.error("Timeout waiting for game window") + _LOGGER.error("Timeout waiting for game window") sys.exit(1) # --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren --- @@ -227,7 +241,7 @@ def main() -> None: for attempt in range(FOCUS_RETRIES): pids = find_process_pids(process_name) if not pids: - logging.warning("Game process gone, stopping") + _LOGGER.warning("Game process gone, stopping") break current_hwnd = find_game_window(pids) @@ -237,10 +251,10 @@ def main() -> None: old_title = win32gui.GetWindowText(hwnd) if hwnd else "" # Nicht von einem Fenster mit Titel auf ein titelloses wechseln if not new_title and old_title: - logging.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)", + _LOGGER.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)", current_hwnd, hwnd, old_title) else: - logging.info("Window changed: old=%s new=%s title=%r", + _LOGGER.info("Window changed: old=%s new=%s title=%r", hwnd, current_hwnd, new_title) hwnd = current_hwnd focused_hwnd = None # neues Fenster muss neu fokussiert werden @@ -258,15 +272,15 @@ def main() -> None: # Warten bis Fenstergröße stabil ist (Resize abgeschlossen) wait_for_stable_size(hwnd) - logging.info("Focus attempt %d/%d for hwnd=%s title=%r", + _LOGGER.info("Focus attempt %d/%d for hwnd=%s title=%r", attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd)) success = bring_to_front(hwnd) if success: - logging.info("Focus succeeded on attempt %d", attempt + 1) + _LOGGER.info("Focus succeeded on attempt %d", attempt + 1) focused_hwnd = hwnd time.sleep(FOCUS_RETRY_DELAY) - logging.info("Done. Final focused hwnd=%s", focused_hwnd) + _LOGGER.info("Done. Final focused hwnd=%s", focused_hwnd) if __name__ == "__main__": diff --git a/launch.vbs b/launch.vbs index eb7656f..1ce41f6 100644 --- a/launch.vbs +++ b/launch.vbs @@ -1,21 +1,50 @@ Set WshShell = CreateObject("WScript.Shell") - -' Prozessname aus dem ersten Argument (Steam %command%) extrahieren -Dim exePath, fso, exeName Set fso = CreateObject("Scripting.FileSystemObject") -exePath = WScript.Arguments(0) + +Dim baseDir, pythonwPath, focusfixPath +baseDir = fso.GetParentFolderName(WScript.ScriptFullName) +pythonwPath = fso.BuildPath(baseDir, ".venv\Scripts\pythonw.exe") +focusfixPath = fso.BuildPath(baseDir, "focusfix.py") + +If Not fso.FileExists(pythonwPath) Then + pythonwPath = "pythonw.exe" +End If + +Dim debugEnabled, exePath, exeName, gameCmd, arg, i +debugEnabled = False +exePath = "" +gameCmd = "" + +For i = 0 To WScript.Arguments.Count - 1 + arg = WScript.Arguments(i) + + If arg = "-d" Or arg = "-debug" Or arg = "--debug" Then + debugEnabled = True + Else + If exePath = "" Then + exePath = arg + End If + gameCmd = gameCmd & """" & arg & """ " + End If +Next + +If exePath = "" Then + WScript.Quit 1 +End If + +' Prozessname aus dem ersten nicht-Wrapper-Argument (Steam %command%) extrahieren exeName = fso.GetBaseName(exePath) ' FocusFix Script unsichtbar starten (pythonw = kein Konsolenfenster) -WshShell.Run """C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe"" ""C:\Projekte\Steam_backgroud_fix\focusfix.py"" --process """ & exeName & """", 0, False +Dim focusfixCmd +focusfixCmd = """" & pythonwPath & """ """ & focusfixPath & """" +If debugEnabled Then + focusfixCmd = focusfixCmd & " -debug" +End If +focusfixCmd = focusfixCmd & " --process """ & exeName & """" +WshShell.Run focusfixCmd, 0, False -' Spiel starten: alle Argumente (= Steam %command%) zusammensetzen und ausführen -Dim gameCmd, i -gameCmd = "" -For i = 0 To WScript.Arguments.Count - 1 - gameCmd = gameCmd & """" & WScript.Arguments(i) & """ " -Next - -If gameCmd <> "" Then +' Spiel starten: alle übrigen Argumente (= Steam %command%) zusammensetzen und ausführen +If Trim(gameCmd) <> "" Then WshShell.Run gameCmd, 1, False End If diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0f21b13 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +psutil +pywin32