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>
156 lines
4.5 KiB
Python
156 lines
4.5 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/>.
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Tuple, Optional, Callable
|
|
|
|
from inputremapper.configs.app_binding import AppBinding
|
|
from inputremapper.configs.input_config import InputCombination
|
|
from inputremapper.configs.mapping import MappingData
|
|
from inputremapper.gui.messages.message_types import (
|
|
MessageType,
|
|
Name,
|
|
Capabilities,
|
|
Key,
|
|
DeviceTypes,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UInputsData:
|
|
message_type = MessageType.uinputs
|
|
uinputs: Dict[Name, Capabilities]
|
|
|
|
def __str__(self):
|
|
string = f"{self.__class__.__name__}(uinputs={self.uinputs})"
|
|
|
|
# find all sequences of comma+space separated numbers, and shorten them
|
|
# to the first and last number
|
|
all_matches = list(re.finditer(r"(\d+, )+", string))
|
|
all_matches.reverse()
|
|
for match in all_matches:
|
|
start = match.start()
|
|
end = match.end()
|
|
start += string[start:].find(",") + 2
|
|
if start == end:
|
|
continue
|
|
string = f"{string[:start]}... {string[end:]}"
|
|
|
|
return string
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GroupsData:
|
|
"""Message containing all available groups and their device types."""
|
|
|
|
message_type = MessageType.groups
|
|
groups: Dict[Key, DeviceTypes]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GroupData:
|
|
"""Message with the active group and available presets for the group."""
|
|
|
|
message_type = MessageType.group
|
|
group_key: str
|
|
presets: Tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PresetData:
|
|
"""Message with the active preset name and mapping names/combinations."""
|
|
|
|
message_type = MessageType.preset
|
|
name: Optional[Name]
|
|
mappings: Optional[Tuple[MappingData, ...]]
|
|
autoload: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StatusData:
|
|
"""Message with the strings and id for the status bar."""
|
|
|
|
message_type = MessageType.status_msg
|
|
ctx_id: int
|
|
msg: Optional[str] = None
|
|
tooltip: Optional[str] = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CombinationRecorded:
|
|
"""Message with the latest recoded combination."""
|
|
|
|
message_type = MessageType.combination_recorded
|
|
combination: "InputCombination"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CombinationUpdate:
|
|
"""Message with the old and new combination (hash for a mapping) when it changed."""
|
|
|
|
message_type = MessageType.combination_update
|
|
old_combination: "InputCombination"
|
|
new_combination: "InputCombination"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UserConfirmRequest:
|
|
"""Message for requesting a user response (confirm/cancel) from the gui."""
|
|
|
|
message_type = MessageType.user_confirm_request
|
|
msg: str
|
|
respond: Callable[[bool], None] = lambda _: None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DoStackSwitch:
|
|
"""Command the stack to switch to a different page."""
|
|
|
|
message_type = MessageType.do_stack_switch
|
|
page_index: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AppBindingsData:
|
|
"""Message with the current focus-driven application bindings and service state.
|
|
|
|
``backend`` is the name of the focus backend reported by the focus-service
|
|
(e.g. "xorg", "sway", "hyprland", "wlr-foreign-toplevel"). ``supported`` is
|
|
False when the service is reachable but reports no usable backend (e.g. on
|
|
GNOME-Wayland). ``reachable`` is False when the focus-service is not running
|
|
yet, in which case the backend state is simply unknown.
|
|
"""
|
|
|
|
message_type = MessageType.app_bindings
|
|
enabled: bool
|
|
bindings: Tuple[AppBinding, ...]
|
|
backend: Optional[str] = None
|
|
supported: bool = False
|
|
reachable: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FocusAppData:
|
|
"""Message with the latest focused application, emitted during detection."""
|
|
|
|
message_type = MessageType.focused_app
|
|
app_id: str
|
|
title: str = ""
|