Solaar/lib/solaar/game_profile_detector.py

165 lines
5.2 KiB
Python

import json
import logging
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
import psutil
CONFIG_PATH = Path.home() / ".config" / "solaar" / "game_dpi_profiles.json"
POLL_SECONDS = 1.0
DEBOUNCE_REQUIRED = 2
COOLDOWN_SECONDS = 5.0
PROFILE_CHOICES = {f"profile {i}": f"Profile {i}" for i in range(1, 6)}
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("solaar-game-profile-detector")
def _load_config():
try:
return json.loads(CONFIG_PATH.read_text())
except Exception:
return {}
def _alive_pids(pids):
alive = set()
for pid in pids:
if psutil.pid_exists(pid):
alive.add(pid)
return alive
def _iter_processes():
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
try:
info = proc.info
name = (info.get("name") or "").lower()
cmdline = [part.lower() for part in (info.get("cmdline") or [])]
yield info.get("pid"), name, cmdline
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
def _match_alias(alias, proc_name, cmdline):
alias = alias.lower()
joined = " ".join(cmdline)
if alias == "minecraft":
return proc_name == "java" and "mojang" in joined
if alias == proc_name:
return True
if alias in proc_name:
return True
return alias in joined
def _find_target(config):
for device_name, device_cfg in config.items():
profiles = device_cfg.get("profiles", [])
for profile in profiles:
if profile.get("is_default"):
continue
aliases = profile.get("aliases", [])
if not aliases:
continue
matched_pids = set()
for pid, proc_name, cmdline in _iter_processes():
if any(_match_alias(alias, proc_name, cmdline) for alias in aliases):
matched_pids.add(pid)
if matched_pids:
return {
"device_name": device_name,
"profile_name": profile.get("name") or profile.get("onboard_profile"),
"onboard_profile": PROFILE_CHOICES.get(profile.get("onboard_profile", "").lower(), profile.get("onboard_profile", "Profile 1")),
"matched_pids": matched_pids,
}
return None
def _default_for_device(config, device_name):
profiles = config.get(device_name, {}).get("profiles", [])
for profile in profiles:
if profile.get("is_default"):
return {
"device_name": device_name,
"profile_name": profile.get("name") or profile.get("onboard_profile"),
"onboard_profile": PROFILE_CHOICES.get(profile.get("onboard_profile", "").lower(), profile.get("onboard_profile", "Profile 1")),
}
return None
def _solaar_cmd():
return shutil.which("solaar") or str(Path.home() / ".local" / "bin" / "solaar")
def _apply_profile(binding):
cmd = [_solaar_cmd(), "config", binding["device_name"], "onboard_profiles", binding["onboard_profile"]]
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def main():
last_applied = None
active_game = None
last_switch = 0.0
pending_key = None
pending_hits = 0
while True:
config = _load_config()
now = time.monotonic()
if active_game is not None:
alive = _alive_pids(active_game.get("matched_pids", set()))
if alive:
active_game["matched_pids"] = alive
time.sleep(POLL_SECONDS)
continue
default_binding = _default_for_device(config, active_game["device_name"])
if default_binding and default_binding != last_applied and now - last_switch >= COOLDOWN_SECONDS:
_apply_profile(default_binding)
logger.info(f"reverted {default_binding['device_name']} to {default_binding['onboard_profile']}")
last_applied = default_binding
last_switch = now
active_game = None
pending_key = None
pending_hits = 0
time.sleep(POLL_SECONDS)
continue
target = _find_target(config)
if target is None:
pending_key = None
pending_hits = 0
time.sleep(POLL_SECONDS)
continue
key = (target["device_name"], target["onboard_profile"])
if key == pending_key:
pending_hits += 1
else:
pending_key = key
pending_hits = 1
if pending_hits >= DEBOUNCE_REQUIRED and target != last_applied and now - last_switch >= COOLDOWN_SECONDS:
_apply_profile(target)
logger.info(f"activated {target['device_name']} -> {target['onboard_profile']} for {target['profile_name']}")
active_game = target
last_applied = {
"device_name": target["device_name"],
"profile_name": target["profile_name"],
"onboard_profile": target["onboard_profile"],
}
last_switch = now
pending_key = None
pending_hits = 0
time.sleep(POLL_SECONDS)
if __name__ == "__main__":
main()