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>
This commit is contained in:
108
inputremapper/configs/app_binding.py
Normal file
108
inputremapper/configs/app_binding.py
Normal file
@ -0,0 +1,108 @@
|
||||
# -*- 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/>.
|
||||
|
||||
"""Models describing which presets should be applied for which application.
|
||||
|
||||
These models are persisted as part of the global config (config.json) and are
|
||||
consumed by the input-remapper-focus-service to decide which presets to inject
|
||||
depending on the currently focused window.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import re
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
try:
|
||||
from pydantic.v1 import BaseModel, Field, root_validator, validator
|
||||
except ImportError:
|
||||
from pydantic import ( # type: ignore[assignment, no-redef]
|
||||
BaseModel,
|
||||
Field,
|
||||
root_validator,
|
||||
validator,
|
||||
)
|
||||
|
||||
from inputremapper.configs.validation_errors import (
|
||||
EmptyAppIdError,
|
||||
InvalidTitleRegexError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from inputremapper.focus.focus_watcher import FocusEvent
|
||||
|
||||
|
||||
class MatchType(str, enum.Enum):
|
||||
"""How an AppBinding decides whether it applies to a focused window."""
|
||||
|
||||
wm_class = "wm_class"
|
||||
title_regex = "title_regex"
|
||||
|
||||
|
||||
def normalize_app_id(app_id: str) -> str:
|
||||
"""Normalize a window identifier for case-insensitive comparison."""
|
||||
return app_id.strip().lower()
|
||||
|
||||
|
||||
class BoundPreset(BaseModel):
|
||||
"""A (device, preset) pair that should be injected for an application."""
|
||||
|
||||
group_key: str
|
||||
preset: str
|
||||
|
||||
@validator("group_key", "preset")
|
||||
def _not_empty(cls, value: str) -> str: # noqa: N805
|
||||
if not value or not value.strip():
|
||||
raise ValueError("group_key and preset must not be empty")
|
||||
return value
|
||||
|
||||
|
||||
class AppBinding(BaseModel):
|
||||
"""Binds a focused application to a list of presets to inject."""
|
||||
|
||||
app_id: str
|
||||
match: MatchType = MatchType.wm_class
|
||||
presets: List[BoundPreset] = Field(default_factory=list)
|
||||
|
||||
@validator("app_id")
|
||||
def _app_id_not_empty(cls, value: str) -> str: # noqa: N805
|
||||
if not value or not value.strip():
|
||||
raise EmptyAppIdError()
|
||||
return value
|
||||
|
||||
@root_validator(skip_on_failure=True)
|
||||
def _valid_regex(cls, values: dict) -> dict: # noqa: N805
|
||||
if values.get("match") == MatchType.title_regex:
|
||||
app_id = values.get("app_id", "")
|
||||
try:
|
||||
re.compile(app_id)
|
||||
except re.error as error:
|
||||
raise InvalidTitleRegexError(app_id, str(error))
|
||||
return values
|
||||
|
||||
def matches(self, event: "FocusEvent") -> bool:
|
||||
"""Whether this binding applies to the given focus event."""
|
||||
if self.match == MatchType.title_regex:
|
||||
try:
|
||||
return re.search(self.app_id, event.title) is not None
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
return normalize_app_id(self.app_id) == normalize_app_id(event.app_id)
|
||||
Reference in New Issue
Block a user