248 lines
7.0 KiB
Python
248 lines
7.0 KiB
Python
import copy
|
|
import json
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
|
|
CONFIG_PATH = Path(__file__).with_name("wheel_bindings.json")
|
|
|
|
ACTION_ORDER = [
|
|
"spotify pause play",
|
|
"spotify next track",
|
|
"spotify previous track",
|
|
"spotify vol up",
|
|
"spotify vol down",
|
|
"spotify mute toggle",
|
|
]
|
|
|
|
ACTION_LABELS = {
|
|
"": "None",
|
|
"spotify pause play": "Pause / Play",
|
|
"spotify next track": "Next Track",
|
|
"spotify previous track": "Previous Track",
|
|
"spotify vol up": "Volume Up",
|
|
"spotify vol down": "Volume Down",
|
|
"spotify mute toggle": "Mute Toggle",
|
|
}
|
|
|
|
DEFAULT_WHEEL_PROFILE_ID = "friend_directinput_wheel"
|
|
|
|
DEFAULT_WHEEL_BINDINGS = {
|
|
"button:33": "spotify vol up",
|
|
"button:31": "spotify vol down",
|
|
"button:22": "spotify next track",
|
|
"button:16": "spotify previous track",
|
|
"button:21": "spotify pause play",
|
|
"button:19": "spotify mute toggle",
|
|
}
|
|
|
|
XBOX_360_BINDINGS = {
|
|
"button:0": "spotify pause play",
|
|
"button:1": "spotify next track",
|
|
"button:2": "spotify previous track",
|
|
"button:3": "spotify vol up",
|
|
"button:4": "spotify vol down",
|
|
"button:5": "spotify mute toggle",
|
|
}
|
|
|
|
|
|
def makeDefaultConfig():
|
|
return {
|
|
"version": 1,
|
|
"default_profile_id": DEFAULT_WHEEL_PROFILE_ID,
|
|
"button_presses_logged": True,
|
|
"global_settings": {
|
|
"debounce_seconds": 0.25,
|
|
"volume_increment": 5,
|
|
"polling_interval_ms": 120,
|
|
},
|
|
"profiles": [
|
|
{
|
|
"id": DEFAULT_WHEEL_PROFILE_ID,
|
|
"name": "Friend DirectInput Wheel",
|
|
"controller": None,
|
|
"bindings": copy.deepcopy(DEFAULT_WHEEL_BINDINGS),
|
|
"button_settings": {},
|
|
},
|
|
{
|
|
"id": "xbox_360_default",
|
|
"name": "Xbox 360 Default",
|
|
"controller": None,
|
|
"bindings": copy.deepcopy(XBOX_360_BINDINGS),
|
|
"button_settings": {},
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def loadConfig():
|
|
if not CONFIG_PATH.exists():
|
|
config = makeDefaultConfig()
|
|
saveConfig(config)
|
|
return config
|
|
|
|
with CONFIG_PATH.open("r", encoding="utf-8") as file:
|
|
data = json.load(file)
|
|
|
|
if isFlatBindingFile(data):
|
|
config = makeDefaultConfig()
|
|
config["profiles"][0]["bindings"] = data
|
|
saveConfig(config)
|
|
return config
|
|
|
|
config = normalizeConfig(data)
|
|
saveConfig(config)
|
|
return config
|
|
|
|
|
|
def saveConfig(config):
|
|
with CONFIG_PATH.open("w", encoding="utf-8") as file:
|
|
json.dump(config, file, indent=4)
|
|
file.write("\n")
|
|
|
|
|
|
def isFlatBindingFile(data):
|
|
if not isinstance(data, dict):
|
|
return False
|
|
|
|
if "profiles" in data:
|
|
return False
|
|
|
|
return all(key.startswith("button:") for key in data.keys())
|
|
|
|
|
|
def normalizeConfig(config):
|
|
defaultConfig = makeDefaultConfig()
|
|
|
|
if not isinstance(config, dict):
|
|
return defaultConfig
|
|
|
|
config.setdefault("version", 1)
|
|
config.setdefault("default_profile_id", DEFAULT_WHEEL_PROFILE_ID)
|
|
config.setdefault("button_presses_logged", True)
|
|
config.setdefault("global_settings", {})
|
|
config["global_settings"].setdefault("debounce_seconds", 0.25)
|
|
config["global_settings"].setdefault("volume_increment", 5)
|
|
config["global_settings"].setdefault("polling_interval_ms", 120)
|
|
config.setdefault("profiles", [])
|
|
|
|
existingProfileIds = {profile.get("id") for profile in config["profiles"] if isinstance(profile, dict)}
|
|
|
|
for defaultProfile in defaultConfig["profiles"]:
|
|
if defaultProfile["id"] not in existingProfileIds:
|
|
config["profiles"].append(defaultProfile)
|
|
|
|
for profile in config["profiles"]:
|
|
profile.setdefault("id", makeProfileId(profile.get("name", "Controller")))
|
|
profile.setdefault("name", "Controller")
|
|
profile.setdefault("controller", None)
|
|
profile.setdefault("bindings", {})
|
|
profile.setdefault("button_settings", {})
|
|
|
|
return config
|
|
|
|
|
|
def getProfile(config, profileId):
|
|
for profile in config.get("profiles", []):
|
|
if profile.get("id") == profileId:
|
|
return profile
|
|
|
|
return None
|
|
|
|
|
|
def getDefaultProfile(config):
|
|
profile = getProfile(config, config.get("default_profile_id"))
|
|
|
|
if profile is not None:
|
|
return profile
|
|
|
|
return config["profiles"][0]
|
|
|
|
|
|
def makeProfileId(profileName):
|
|
cleaned = "".join(character.lower() if character.isalnum() else "_" for character in profileName)
|
|
cleaned = "_".join(part for part in cleaned.split("_") if part)
|
|
|
|
if not cleaned:
|
|
cleaned = "controller"
|
|
|
|
return f"{cleaned}_{uuid.uuid4().hex[:8]}"
|
|
|
|
|
|
def controllerFromJoystick(joystick):
|
|
joystick.init()
|
|
|
|
return {
|
|
"index": joystick.get_id(),
|
|
"instance_id": joystick.get_instance_id(),
|
|
"name": joystick.get_name(),
|
|
"guid": joystick.get_guid(),
|
|
"numaxes": joystick.get_numaxes(),
|
|
"numbuttons": joystick.get_numbuttons(),
|
|
"numhats": joystick.get_numhats(),
|
|
}
|
|
|
|
|
|
def stableControllerIdentity(controller):
|
|
return {
|
|
"name": controller.get("name"),
|
|
"guid": controller.get("guid"),
|
|
"numaxes": controller.get("numaxes"),
|
|
"numbuttons": controller.get("numbuttons"),
|
|
"numhats": controller.get("numhats"),
|
|
}
|
|
|
|
|
|
def profileHasController(profile):
|
|
controller = profile.get("controller")
|
|
|
|
return isinstance(controller, dict) and bool(controller.get("name") or controller.get("guid"))
|
|
|
|
|
|
def controllerMatches(profile, controller):
|
|
storedController = profile.get("controller")
|
|
|
|
if not isinstance(storedController, dict):
|
|
return False
|
|
|
|
if storedController.get("guid") and controller.get("guid"):
|
|
return storedController.get("guid") == controller.get("guid")
|
|
|
|
return (
|
|
storedController.get("name") == controller.get("name")
|
|
and storedController.get("numaxes") == controller.get("numaxes")
|
|
and storedController.get("numbuttons") == controller.get("numbuttons")
|
|
and storedController.get("numhats") == controller.get("numhats")
|
|
)
|
|
|
|
|
|
def findMatchingProfile(config, controller):
|
|
for profile in config.get("profiles", []):
|
|
if controllerMatches(profile, controller):
|
|
return profile
|
|
|
|
return None
|
|
|
|
|
|
def saveControllerProfile(config, controller, profileName, bindings, buttonSettings=None, existingProfileId=None):
|
|
profile = getProfile(config, existingProfileId) if existingProfileId else None
|
|
|
|
if profile is None or not profileHasController(profile):
|
|
profile = {
|
|
"id": makeProfileId(profileName),
|
|
"name": profileName,
|
|
"controller": stableControllerIdentity(controller),
|
|
"bindings": {},
|
|
"button_settings": {},
|
|
}
|
|
config["profiles"].append(profile)
|
|
|
|
profile["name"] = profileName
|
|
profile["controller"] = stableControllerIdentity(controller)
|
|
profile["bindings"] = copy.deepcopy(bindings)
|
|
profile["button_settings"] = copy.deepcopy(buttonSettings or {})
|
|
|
|
saveConfig(config)
|
|
|
|
return profile
|