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>
698 lines
25 KiB
Python
698 lines
25 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 glob
|
|
import os
|
|
import re
|
|
import time
|
|
from typing import Optional, List, Tuple, Set, Dict
|
|
|
|
from gi.repository import GLib
|
|
|
|
from inputremapper.configs.app_binding import AppBinding
|
|
from inputremapper.configs.global_config import GlobalConfig
|
|
from inputremapper.configs.input_config import InputCombination, InputConfig
|
|
from inputremapper.configs.mapping import UIMapping, MappingData
|
|
from inputremapper.configs.paths import PathUtils
|
|
from inputremapper.configs.preset import Preset
|
|
from inputremapper.configs.keyboard_layout import KeyboardLayout
|
|
from inputremapper.daemon import DaemonProxy
|
|
from inputremapper.exceptions import DataManagementError
|
|
from inputremapper.groups import _Group
|
|
from inputremapper.gui.gettext import _
|
|
from inputremapper.gui.messages.message_broker import (
|
|
MessageBroker,
|
|
)
|
|
from inputremapper.gui.messages.message_data import (
|
|
UInputsData,
|
|
GroupData,
|
|
PresetData,
|
|
CombinationUpdate,
|
|
AppBindingsData,
|
|
FocusAppData,
|
|
)
|
|
from inputremapper.gui.focus_service_client import FocusServiceClient
|
|
from inputremapper.gui.reader_client import ReaderClient
|
|
from inputremapper.injection.global_uinputs import GlobalUInputs
|
|
from inputremapper.injection.injector import (
|
|
InjectorState,
|
|
InjectorStateMessage,
|
|
)
|
|
from inputremapper.logging.logger import logger
|
|
|
|
DEFAULT_PRESET_NAME = _("new preset")
|
|
|
|
# useful type aliases
|
|
Name = str
|
|
GroupKey = str
|
|
|
|
|
|
class DataManager:
|
|
"""DataManager provides an interface to create and modify configurations as well
|
|
as modify the state of the Service.
|
|
|
|
Any state changes will be announced via the MessageBroker.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message_broker: MessageBroker,
|
|
config: GlobalConfig,
|
|
reader_client: ReaderClient,
|
|
daemon: DaemonProxy,
|
|
uinputs: GlobalUInputs,
|
|
keyboard_layout: KeyboardLayout,
|
|
focus_service: Optional[FocusServiceClient] = None,
|
|
):
|
|
self.message_broker = message_broker
|
|
self._reader_client = reader_client
|
|
self._daemon = daemon
|
|
self._uinputs = uinputs
|
|
self._keyboard_layout = keyboard_layout
|
|
self._focus_service = focus_service or FocusServiceClient()
|
|
uinputs.prepare_all()
|
|
|
|
self._config = config
|
|
self._config.load_config()
|
|
|
|
self._active_preset: Optional[Preset[UIMapping]] = None
|
|
self._active_mapping: Optional[UIMapping] = None
|
|
self._active_input_config: Optional[InputConfig] = None
|
|
|
|
def publish_group(self):
|
|
"""Send active group to the MessageBroker.
|
|
|
|
This is internally called whenever the group changes.
|
|
It is usually not necessary to call this explicitly from
|
|
outside DataManager.
|
|
"""
|
|
self.message_broker.publish(
|
|
GroupData(self.active_group.key, self.get_preset_names())
|
|
)
|
|
|
|
def publish_preset(self):
|
|
"""Send active preset to the MessageBroker.
|
|
|
|
This is internally called whenever the preset changes.
|
|
It is usually not necessary to call this explicitly from
|
|
outside DataManager.
|
|
"""
|
|
self.message_broker.publish(
|
|
PresetData(
|
|
self.active_preset.name, self.get_mappings(), self.get_autoload()
|
|
)
|
|
)
|
|
|
|
def publish_mapping(self):
|
|
"""Send active mapping to the MessageBroker
|
|
|
|
This is internally called whenever the mapping changes.
|
|
It is usually not necessary to call this explicitly from
|
|
outside DataManager.
|
|
"""
|
|
if self.active_mapping:
|
|
self.message_broker.publish(self.active_mapping.get_bus_message())
|
|
|
|
def publish_event(self):
|
|
"""Send active event to the MessageBroker.
|
|
|
|
This is internally called whenever the event changes.
|
|
It is usually not necessary to call this explicitly from
|
|
outside DataManager
|
|
"""
|
|
if self.active_input_config:
|
|
assert self.active_input_config in self.active_mapping.input_combination
|
|
self.message_broker.publish(self.active_input_config)
|
|
|
|
def publish_uinputs(self):
|
|
"""Send the "uinputs" message on the MessageBroker."""
|
|
self.message_broker.publish(
|
|
UInputsData(
|
|
{
|
|
name: uinput.capabilities()
|
|
for name, uinput in self._uinputs.devices.items()
|
|
}
|
|
)
|
|
)
|
|
|
|
def publish_groups(self):
|
|
"""Publish the "groups" message on the MessageBroker."""
|
|
self._reader_client.publish_groups()
|
|
|
|
def publish_injector_state(self):
|
|
"""Publish the "injector_state" message for the active_group."""
|
|
if not self.active_group:
|
|
return
|
|
|
|
self.message_broker.publish(InjectorStateMessage(self.get_state()))
|
|
|
|
def publish_app_bindings(self):
|
|
"""Publish the "app_bindings" message with the current service state."""
|
|
status = self._focus_service.get_status()
|
|
reachable = status is not None
|
|
backend = status.get("backend") if status else None
|
|
self.message_broker.publish(
|
|
AppBindingsData(
|
|
enabled=self._config.get_app_binding_enabled(),
|
|
bindings=tuple(self._config.get_app_bindings()),
|
|
backend=backend,
|
|
supported=reachable and backend is not None,
|
|
reachable=reachable,
|
|
)
|
|
)
|
|
|
|
def get_app_bindings(self) -> List[AppBinding]:
|
|
"""Get the configured application bindings."""
|
|
return self._config.get_app_bindings()
|
|
|
|
def set_app_bindings(self, bindings: List[AppBinding]):
|
|
"""Persist the application bindings and reconcile the focus-service.
|
|
|
|
Will send the "app_bindings" message on the MessageBroker.
|
|
"""
|
|
self._config.set_app_bindings(bindings)
|
|
self._focus_service.reload()
|
|
self.publish_app_bindings()
|
|
|
|
def get_app_binding_enabled(self) -> bool:
|
|
"""Whether focus-driven preset binding is enabled."""
|
|
return self._config.get_app_binding_enabled()
|
|
|
|
def set_app_binding_enabled(self, enabled: bool):
|
|
"""Enable or disable focus-driven preset binding.
|
|
|
|
Writes the config and toggles the running service. Will send the
|
|
"app_bindings" message on the MessageBroker.
|
|
"""
|
|
self._config.set_app_binding_enabled(enabled)
|
|
self._focus_service.set_enabled(enabled)
|
|
self.publish_app_bindings()
|
|
|
|
def start_app_detection(self):
|
|
"""Listen for focus changes to auto-fill an app_id.
|
|
|
|
Will send "focused_app" messages as the focus changes.
|
|
"""
|
|
self._focus_service.connect_focus_changed(self._on_focus_changed)
|
|
|
|
def stop_app_detection(self):
|
|
"""Stop listening for focus changes."""
|
|
self._focus_service.disconnect_focus_changed()
|
|
|
|
def _on_focus_changed(self, app: Dict[str, str]):
|
|
self.message_broker.publish(
|
|
FocusAppData(
|
|
app_id=app.get("app_id", ""),
|
|
title=app.get("title", ""),
|
|
)
|
|
)
|
|
|
|
def get_presets_for_group(self, group_key: str) -> Tuple[Name, ...]:
|
|
"""Get all preset names for an arbitrary group, sorted by age.
|
|
|
|
Unlike get_preset_names this does not depend on the active group, so it
|
|
can be used to populate the application-binding preset selectors.
|
|
"""
|
|
group = self._reader_client.groups.find(key=group_key)
|
|
if not group:
|
|
return tuple()
|
|
|
|
device_folder = PathUtils.get_preset_path(group.name)
|
|
PathUtils.mkdir(device_folder)
|
|
|
|
paths = glob.glob(os.path.join(glob.escape(device_folder), "*.json"))
|
|
presets = [
|
|
os.path.splitext(os.path.basename(path))[0]
|
|
for path in sorted(paths, key=os.path.getmtime)
|
|
]
|
|
presets.reverse()
|
|
return tuple(presets)
|
|
|
|
@property
|
|
def active_group(self) -> Optional[_Group]:
|
|
"""The currently loaded group."""
|
|
return self._reader_client.group
|
|
|
|
@property
|
|
def active_preset(self) -> Optional[Preset[UIMapping]]:
|
|
"""The currently loaded preset."""
|
|
return self._active_preset
|
|
|
|
@property
|
|
def active_mapping(self) -> Optional[UIMapping]:
|
|
"""The currently loaded mapping."""
|
|
return self._active_mapping
|
|
|
|
@property
|
|
def active_input_config(self) -> Optional[InputConfig]:
|
|
"""The currently loaded event."""
|
|
return self._active_input_config
|
|
|
|
def get_group_keys(self) -> Tuple[GroupKey, ...]:
|
|
"""Get all group keys (plugged devices)."""
|
|
return tuple(group.key for group in self._reader_client.groups.get_groups())
|
|
|
|
def get_preset_names(self) -> Tuple[Name, ...]:
|
|
"""Get all preset names for active_group and current user sorted by age."""
|
|
if not self.active_group:
|
|
raise DataManagementError("Cannot find presets: Group is not set")
|
|
device_folder = PathUtils.get_preset_path(self.active_group.name)
|
|
PathUtils.mkdir(device_folder)
|
|
|
|
paths = glob.glob(os.path.join(glob.escape(device_folder), "*.json"))
|
|
presets = [
|
|
os.path.splitext(os.path.basename(path))[0]
|
|
for path in sorted(paths, key=os.path.getmtime)
|
|
]
|
|
# the highest timestamp to the front
|
|
presets.reverse()
|
|
return tuple(presets)
|
|
|
|
def get_mappings(self) -> Optional[List[MappingData]]:
|
|
"""All mappings from the active_preset."""
|
|
if not self._active_preset:
|
|
return None
|
|
|
|
return [mapping.get_bus_message() for mapping in self._active_preset]
|
|
|
|
def get_autoload(self) -> bool:
|
|
"""The autoload status of the active_preset."""
|
|
if not self.active_preset or not self.active_group:
|
|
return False
|
|
return self._config.is_autoloaded(
|
|
self.active_group.key, self.active_preset.name
|
|
)
|
|
|
|
def set_autoload(self, status: bool):
|
|
"""Set the autoload status of the active_preset.
|
|
|
|
Will send "preset" message on the MessageBroker.
|
|
"""
|
|
if not self.active_preset or not self.active_group:
|
|
raise DataManagementError("Cannot set autoload status: Preset is not set")
|
|
|
|
if status:
|
|
self._config.set_autoload_preset(
|
|
self.active_group.key, self.active_preset.name
|
|
)
|
|
elif self.get_autoload():
|
|
self._config.set_autoload_preset(self.active_group.key, None)
|
|
|
|
self.publish_preset()
|
|
|
|
def get_newest_group_key(self) -> GroupKey:
|
|
"""group_key of the group with the most recently modified preset."""
|
|
paths = []
|
|
pattern = os.path.join(
|
|
glob.escape(PathUtils.get_preset_path()),
|
|
"*/*.json",
|
|
)
|
|
for path in glob.glob(pattern):
|
|
if self._reader_client.groups.find(key=PathUtils.split_all(path)[-2]):
|
|
paths.append((path, os.path.getmtime(path)))
|
|
|
|
if not paths:
|
|
raise FileNotFoundError()
|
|
|
|
path, _ = max(paths, key=lambda x: x[1])
|
|
return PathUtils.split_all(path)[-2]
|
|
|
|
def get_newest_preset_name(self) -> Name:
|
|
"""Preset name of the most recently modified preset in the active group."""
|
|
if not self.active_group:
|
|
raise DataManagementError("Cannot find newest preset: Group is not set")
|
|
|
|
pattern = os.path.join(
|
|
glob.escape(PathUtils.get_preset_path(self.active_group.name)),
|
|
"*.json",
|
|
)
|
|
paths = [(path, os.path.getmtime(path)) for path in glob.glob(pattern)]
|
|
if not paths:
|
|
raise FileNotFoundError()
|
|
|
|
path, _ = max(paths, key=lambda x: x[1])
|
|
return os.path.split(path)[-1].split(".")[0]
|
|
|
|
def get_available_preset_name(self, name=DEFAULT_PRESET_NAME) -> Name:
|
|
"""The first available preset in the active group."""
|
|
if not self.active_group:
|
|
raise DataManagementError("Unable find preset name. Group is not set")
|
|
|
|
name = name.strip()
|
|
|
|
# find a name that is not already taken
|
|
if os.path.exists(PathUtils.get_preset_path(self.active_group.name, name)):
|
|
# if there already is a trailing number, increment it instead of
|
|
# adding another one
|
|
match = re.match(r"^(.+) (\d+)$", name)
|
|
if match:
|
|
name = match[1]
|
|
i = int(match[2]) + 1
|
|
else:
|
|
i = 2
|
|
|
|
while os.path.exists(
|
|
PathUtils.get_preset_path(self.active_group.name, f"{name} {i}")
|
|
):
|
|
i += 1
|
|
|
|
return f"{name} {i}"
|
|
|
|
return name
|
|
|
|
def load_group(self, group_key: str):
|
|
"""Load a group. will publish "groups" and "injector_state" messages.
|
|
|
|
This will render the active_mapping and active_preset invalid.
|
|
"""
|
|
if group_key not in self.get_group_keys():
|
|
raise DataManagementError("Unable to load non existing group")
|
|
|
|
logger.info('Loading group "%s"', group_key)
|
|
|
|
self._active_input_config = None
|
|
self._active_mapping = None
|
|
self._active_preset = None
|
|
group = self._reader_client.groups.find(key=group_key)
|
|
self._reader_client.set_group(group)
|
|
self.publish_group()
|
|
self.publish_injector_state()
|
|
|
|
def load_preset(self, name: str):
|
|
"""Load a preset. Will send "preset" message on the MessageBroker.
|
|
|
|
This will render the active_mapping invalid.
|
|
"""
|
|
if not self.active_group:
|
|
raise DataManagementError("Unable to load preset. Group is not set")
|
|
|
|
logger.info('Loading preset "%s"', name)
|
|
|
|
preset_path = PathUtils.get_preset_path(self.active_group.name, name)
|
|
preset = Preset(preset_path, mapping_factory=UIMapping)
|
|
preset.load()
|
|
self._active_input_config = None
|
|
self._active_mapping = None
|
|
self._active_preset = preset
|
|
self.publish_preset()
|
|
|
|
def load_mapping(self, combination: InputCombination):
|
|
"""Load a mapping. Will send "mapping" message on the MessageBroker."""
|
|
if not self._active_preset:
|
|
raise DataManagementError("Unable to load mapping. Preset is not set")
|
|
|
|
mapping = self._active_preset.get_mapping(combination)
|
|
if not mapping:
|
|
msg = (
|
|
f"the mapping with {combination = } does not "
|
|
f"exist in the {self._active_preset.path}"
|
|
)
|
|
logger.error(msg)
|
|
raise KeyError(msg)
|
|
|
|
self._active_input_config = None
|
|
self._active_mapping = mapping
|
|
self.publish_mapping()
|
|
|
|
def load_input_config(self, input_config: InputConfig):
|
|
"""Load a InputConfig from the combination in the active mapping.
|
|
|
|
Will send "event" message on the MessageBroker,
|
|
"""
|
|
if not self.active_mapping:
|
|
raise DataManagementError("Unable to load event. Mapping is not set")
|
|
if input_config not in self.active_mapping.input_combination:
|
|
raise ValueError(
|
|
f"{input_config} is not member of active_mapping.input_combination: "
|
|
f"{self.active_mapping.input_combination}"
|
|
)
|
|
self._active_input_config = input_config
|
|
self.publish_event()
|
|
|
|
def rename_preset(self, new_name: str):
|
|
"""Rename the current preset and move the correct file.
|
|
|
|
Will send "group" and then "preset" message on the MessageBroker
|
|
"""
|
|
if not self.active_preset or not self.active_group:
|
|
raise DataManagementError("Unable rename preset: Preset is not set")
|
|
|
|
if self.active_preset.path == PathUtils.get_preset_path(
|
|
self.active_group.name, new_name
|
|
):
|
|
return
|
|
|
|
old_path = self.active_preset.path
|
|
assert old_path is not None
|
|
old_name = os.path.basename(old_path).split(".")[0]
|
|
new_path = PathUtils.get_preset_path(self.active_group.name, new_name)
|
|
if os.path.exists(new_path):
|
|
raise ValueError(
|
|
f"cannot rename {old_name} to " f"{new_name}, preset already exists"
|
|
)
|
|
|
|
logger.info('Moving "%s" to "%s"', old_path, new_path)
|
|
os.rename(old_path, new_path)
|
|
now = time.time()
|
|
os.utime(new_path, (now, now))
|
|
|
|
if self._config.is_autoloaded(self.active_group.key, old_name):
|
|
self._config.set_autoload_preset(self.active_group.key, new_name)
|
|
|
|
self.active_preset.path = PathUtils.get_preset_path(
|
|
self.active_group.name, new_name
|
|
)
|
|
self.publish_group()
|
|
self.publish_preset()
|
|
|
|
def copy_preset(self, name: str):
|
|
"""Copy the current preset to the given name.
|
|
|
|
Will send "group" and "preset" message to the MessageBroker and load the copy
|
|
"""
|
|
# todo: Do we want to load the copy here? or is this up to the controller?
|
|
if not self.active_preset or not self.active_group:
|
|
raise DataManagementError("Unable to copy preset: Preset is not set")
|
|
|
|
if self.active_preset.path == PathUtils.get_preset_path(
|
|
self.active_group.name, name
|
|
):
|
|
return
|
|
|
|
if name in self.get_preset_names():
|
|
raise ValueError(f"a preset with the name {name} already exits")
|
|
|
|
new_path = PathUtils.get_preset_path(self.active_group.name, name)
|
|
logger.info('Copy "%s" to "%s"', self.active_preset.path, new_path)
|
|
self.active_preset.path = new_path
|
|
self.save()
|
|
self.publish_group()
|
|
self.publish_preset()
|
|
|
|
def create_preset(self, name: str):
|
|
"""Create empty preset in the active_group.
|
|
|
|
Will send "group" message to the MessageBroker
|
|
"""
|
|
if not self.active_group:
|
|
raise DataManagementError("Unable to add preset. Group is not set")
|
|
|
|
path = PathUtils.get_preset_path(self.active_group.name, name)
|
|
if os.path.exists(path):
|
|
raise DataManagementError("Unable to add preset. Preset exists")
|
|
|
|
Preset(path).save()
|
|
self.publish_group()
|
|
|
|
def delete_preset(self):
|
|
"""Delete the active preset.
|
|
|
|
Will send "group" message to the MessageBroker
|
|
this will invalidate the active mapping,
|
|
"""
|
|
preset_path = self._active_preset.path
|
|
logger.info('Removing "%s"', preset_path)
|
|
os.remove(preset_path)
|
|
self._active_mapping = None
|
|
self._active_preset = None
|
|
self.publish_group()
|
|
|
|
def update_mapping(self, **kwargs):
|
|
"""Update the active mapping with the given keywords and values.
|
|
|
|
Will send "mapping" message to the MessageBroker. In case of a new
|
|
input_combination. This will first send a "combination_update" message.
|
|
"""
|
|
if not self._active_mapping:
|
|
raise DataManagementError("Cannot modify Mapping: Mapping is not set")
|
|
|
|
if symbol := kwargs.get("output_symbol"):
|
|
kwargs["output_symbol"] = self._keyboard_layout.correct_case(symbol)
|
|
|
|
combination = self.active_mapping.input_combination
|
|
for key, value in kwargs.items():
|
|
setattr(self._active_mapping, key, value)
|
|
|
|
if (
|
|
"input_combination" in kwargs
|
|
and combination != self.active_mapping.input_combination
|
|
):
|
|
self._active_input_config = None
|
|
self.message_broker.publish(
|
|
CombinationUpdate(combination, self._active_mapping.input_combination)
|
|
)
|
|
|
|
if "mapping_type" in kwargs:
|
|
# mapping_type must be the last update because it is automatically updated
|
|
# by a validation function
|
|
self._active_mapping.mapping_type = kwargs["mapping_type"]
|
|
|
|
self.publish_mapping()
|
|
|
|
def update_input_config(self, new_input_config: InputConfig):
|
|
"""Update the active input configuration.
|
|
|
|
Will send "combination_update", "mapping" and "event" messages to the
|
|
MessageBroker (in that order)
|
|
"""
|
|
if not self.active_mapping or not self.active_input_config:
|
|
raise DataManagementError("Cannot modify event: Event is not set")
|
|
|
|
combination = list(self.active_mapping.input_combination)
|
|
combination[combination.index(self.active_input_config)] = new_input_config
|
|
self.update_mapping(input_combination=InputCombination(combination))
|
|
self._active_input_config = new_input_config
|
|
self.publish_event()
|
|
|
|
def create_mapping(self):
|
|
"""Create empty mapping in the active preset.
|
|
|
|
Will send "preset" message to the MessageBroker
|
|
"""
|
|
if not self._active_preset:
|
|
raise DataManagementError("Cannot create mapping: Preset is not set")
|
|
self._active_preset.add(UIMapping())
|
|
self.publish_preset()
|
|
|
|
def delete_mapping(self):
|
|
"""Delete the active mapping.
|
|
|
|
Will send "preset" message to the MessageBroker
|
|
"""
|
|
if not self._active_mapping:
|
|
raise DataManagementError(
|
|
"cannot delete active mapping: active mapping is not set"
|
|
)
|
|
|
|
self._active_preset.remove(self._active_mapping.input_combination)
|
|
self._active_mapping = None
|
|
self.publish_preset()
|
|
|
|
def save(self):
|
|
"""Save the active preset."""
|
|
if self._active_preset:
|
|
self._active_preset.save()
|
|
|
|
def refresh_groups(self):
|
|
"""Refresh the groups (plugged devices).
|
|
|
|
Should send "groups" message to MessageBroker this will not happen immediately
|
|
because the system might take a bit until the groups are available
|
|
"""
|
|
self._reader_client.refresh_groups()
|
|
|
|
def start_combination_recording(self):
|
|
"""Record user input.
|
|
|
|
Will send "combination_recorded" messages as new input arrives.
|
|
Will eventually send a "recording_finished" message.
|
|
"""
|
|
self._reader_client.start_recorder()
|
|
|
|
def stop_combination_recording(self):
|
|
"""Stop recording user input.
|
|
|
|
Will send a recording_finished signal if a recording is running.
|
|
"""
|
|
self._reader_client.stop_recorder()
|
|
|
|
def stop_injecting(self) -> None:
|
|
"""Stop injecting for the active group.
|
|
|
|
Will send "injector_state" message once the injector has stopped."""
|
|
if not self.active_group:
|
|
raise DataManagementError("Cannot stop injection: Group is not set")
|
|
self._daemon.stop_injecting(self.active_group.key)
|
|
self.do_when_injector_state(
|
|
{InjectorState.STOPPED}, self.publish_injector_state
|
|
)
|
|
|
|
def start_injecting(self) -> bool:
|
|
"""Start injecting the active preset for the active group.
|
|
|
|
returns if the startup was successfully initialized.
|
|
Will send "injector_state" message once the startup is complete.
|
|
"""
|
|
if not self.active_preset or not self.active_group:
|
|
raise DataManagementError("Cannot start injection: Preset is not set")
|
|
|
|
self._daemon.set_config_dir(self._config.get_dir())
|
|
assert self.active_preset.name is not None
|
|
if self._daemon.start_injecting(self.active_group.key, self.active_preset.name):
|
|
self.do_when_injector_state(
|
|
{
|
|
InjectorState.RUNNING,
|
|
InjectorState.ERROR,
|
|
InjectorState.NO_GRAB,
|
|
InjectorState.UPGRADE_EVDEV,
|
|
},
|
|
self.publish_injector_state,
|
|
)
|
|
return True
|
|
return False
|
|
|
|
def get_state(self) -> InjectorState:
|
|
"""The state of the injector."""
|
|
if not self.active_group:
|
|
raise DataManagementError("Cannot read state: Group is not set")
|
|
return self._daemon.get_state(self.active_group.key)
|
|
|
|
def refresh_service_config_path(self):
|
|
"""Tell the service to refresh its config path."""
|
|
self._daemon.set_config_dir(self._config.get_dir())
|
|
|
|
def do_when_injector_state(self, states: Set[InjectorState], callback):
|
|
"""Run callback once the injector state is one of states."""
|
|
start = time.time()
|
|
|
|
def do():
|
|
if time.time() - start > 3:
|
|
# something went wrong, there should have been a state long ago.
|
|
# the timeout prevents tons of GLib.timeouts to run forever, especially
|
|
# after spamming the "Stop" button.
|
|
logger.error("Timed out while waiting for injector state %s", states)
|
|
return False
|
|
|
|
if self.get_state() in states:
|
|
callback()
|
|
return False
|
|
return True
|
|
|
|
GLib.timeout_add(100, do)
|