Files
InputRemapper/inputremapper/configs/global_config.py
Blomios 9ef2c1439c feat(app-binding): lier les presets aux applications
Ajoute la configuration des associations application/preset, le service de suivi du focus, l'intégration GUI et les fichiers d'installation nécessaires.

Ajoute les tests unitaires ciblés des bindings, du service de focus, des migrations et des composants GUI associés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 23:52:34 +02:00

172 lines
6.1 KiB
Python

# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# This file is part of input-remapper.
#
# input-remapper 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.
#
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.
"""Store which presets should be enabled for which device on login."""
from __future__ import annotations
import copy
import json
import os
from typing import List, Optional
from inputremapper.configs.app_binding import AppBinding
from inputremapper.configs.paths import PathUtils
from inputremapper.logging.logger import logger, VERSION
from inputremapper.user import UserUtils
MOUSE = "mouse"
WHEEL = "wheel"
BUTTONS = "buttons"
NONE = "none"
INITIAL_CONFIG = {
"version": VERSION,
"autoload": {},
"app_binding_enabled": False,
"app_bindings": [],
}
class GlobalConfig:
"""Configures stuff like autoloading in ~/.config/input-remapper-2/config.json."""
def __init__(self):
self.path = os.path.join(PathUtils.config_path(), "config.json")
self._config = copy.deepcopy(INITIAL_CONFIG)
def get_dir(self) -> str:
"""The folder containing this config."""
return os.path.split(self.path)[0]
def get_autoload_preset(self, group_key: str) -> Optional[str]:
# modifications are only allowed via the setter, because it needs to write
# the config file too. Therefore return a copy to prevent inconsistencies.
return copy.deepcopy(self._config["autoload"].get(group_key))
def set_autoload_preset(self, group_key: str, preset: Optional[str]):
"""Set a preset to be automatically applied on start.
Parameters
----------
group_key
the unique identifier of the group. This is used instead of the
name to enable autoloading two different presets when two similar
devices are connected.
preset
if None, don't autoload something for this device.
"""
if preset is not None:
self._config["autoload"][group_key] = preset
else:
logger.info('Not injecting for "%s" automatically anmore', group_key)
del self._config["autoload"][group_key]
self._save_config()
def iterate_autoload_presets(self):
"""Get tuples of (device, preset)."""
return self._config.get("autoload", {}).items()
def is_autoloaded(self, group_key: Optional[str], preset: Optional[str]):
"""Should this preset be loaded automatically?"""
if group_key is None or preset is None:
raise ValueError("Expected group_key and preset to not be None")
return self._config.get("autoload", {}).get(group_key) == preset
def get_app_binding_enabled(self) -> bool:
"""Whether focus-driven preset binding is enabled."""
return bool(self._config.get("app_binding_enabled", False))
def set_app_binding_enabled(self, enabled: bool) -> None:
"""Enable or disable focus-driven preset binding."""
self._config["app_binding_enabled"] = bool(enabled)
self._save_config()
def get_app_bindings(self) -> List[AppBinding]:
"""Get the configured application bindings as model objects."""
bindings = []
for raw in self._config.get("app_bindings", []):
try:
bindings.append(AppBinding(**raw))
except (TypeError, ValueError) as error:
logger.error("Ignoring invalid app_binding %s: %s", raw, str(error))
return bindings
def set_app_bindings(self, bindings: List[AppBinding]) -> None:
"""Persist the given application bindings."""
self._config["app_bindings"] = [
json.loads(binding.json()) for binding in bindings
]
self._save_config()
def load_config(self, path: Optional[str] = None):
"""Load the config from the file system.
Parameters
----------
path
If set, will change the path to load from and save to.
"""
if path is not None:
if not os.path.exists(path):
logger.error('Config at "%s" not found', path)
return
self.path = path
self._clear_config()
if not os.path.exists(self.path):
# treated like an empty config
logger.debug('Config "%s" doesn\'t exist yet', self.path)
self._clear_config()
self._config = copy.deepcopy(INITIAL_CONFIG)
self._save_config()
return
with open(self.path, "r") as file:
try:
self._config.update(json.load(file))
logger.info('Loaded config from "%s"', self.path)
except json.decoder.JSONDecodeError as error:
logger.error(
'Failed to parse config "%s": %s. Using defaults',
self.path,
str(error),
)
# uses the default configuration when the config object
# is empty automatically
def _save_config(self):
"""Save the config to the file system."""
if UserUtils.user == "root":
logger.debug("Skipping config file creation for the root user")
return
PathUtils.touch(self.path)
with open(self.path, "w") as file:
json.dump(self._config, file, indent=4)
logger.info("Saved config to %s", self.path)
file.write("\n")
def _clear_config(self):
"""Remove all configurations in memory."""
self._config = {}