chore(repo): baseline du fork input-remapper (upstream v2.2.1)

Le dépôt ne contenait que README.md ; ajout de l'intégralité du code
fonctionnel du fork comme socle stable de la branche de release.
L'état runtime d'orchestration (.ideai/) est exclu du suivi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 00:47:12 +02:00
parent 1d790a78fb
commit 0ab77d2a64
257 changed files with 51859 additions and 0 deletions

View File

View File

@ -0,0 +1,143 @@
# -*- 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/>.
"""Logging setup for input-remapper."""
import logging
import os
import sys
from datetime import datetime
from typing import Dict
class ColorfulFormatter(logging.Formatter):
"""Overwritten Formatter to print nicer logs.
It colors all logs from the same filename in the same color to visually group them
together. It also adds process name, process id, file, line-number and time.
If debug mode is not active, it will not do any of this.
"""
def __init__(self, debug_mode: bool = False):
super().__init__()
self.debug_mode = debug_mode
self.file_color_mapping: Dict[str, int] = {}
# see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
self.allowed_colors = []
for r in range(0, 6):
for g in range(0, 6):
for b in range(0, 6):
# https://stackoverflow.com/a/596243
brightness = 0.2126 * r + 0.7152 * g + 0.0722 * b
if brightness < 1:
# prefer light colors, because most people have a dark
# terminal background
continue
if g + b <= 1:
# red makes it look like it's an error
continue
if abs(g - b) < 2 and abs(b - r) < 2 and abs(r - g) < 2:
# no colors that are too grey
continue
self.allowed_colors.append(self._get_ansi_code(r, g, b))
self.level_based_colors = {
logging.WARNING: 11,
logging.ERROR: 9,
logging.FATAL: 9,
}
def _get_ansi_code(self, r: int, g: int, b: int) -> int:
return 16 + b + (6 * g) + (36 * r)
def _word_to_color(self, word: str) -> int:
"""Convert a word to a 8bit ansi color code."""
digit_sum = sum([ord(char) for char in word])
index = digit_sum % len(self.allowed_colors)
return self.allowed_colors[index]
def _allocate_debug_log_color(self, record: logging.LogRecord):
"""Get the color that represents the source file of the log."""
if self.file_color_mapping.get(record.filename) is not None:
return self.file_color_mapping[record.filename]
color = self._word_to_color(record.filename)
if self.file_color_mapping.get(record.filename) is None:
# calculate the color for each file only once
self.file_color_mapping[record.filename] = color
return color
def _get_process_name(self):
"""Generate a beaitiful to read name for this process."""
process_path = sys.argv[0]
process_name = process_path.split("/")[-1]
if "input-remapper-" in process_name:
process_name = process_name.replace("input-remapper-", "")
if process_name == "gtk":
process_name = "GUI"
return process_name
def _get_format(self, record: logging.LogRecord):
"""Generate a message format string."""
if record.levelno == logging.INFO and not self.debug_mode:
# if not launched with --debug, then don't print "INFO:"
return "%(message)s"
if not self.debug_mode:
color = self.level_based_colors.get(record.levelno, 9)
return f"\033[38;5;{color}m%(levelname)s\033[0m: %(message)s"
color = self._allocate_debug_log_color(record)
if record.levelno in [logging.ERROR, logging.WARNING, logging.FATAL]:
# underline
style = f"\033[4;38;5;{color}m"
else:
style = f"\033[38;5;{color}m"
process_color = self._word_to_color(f"{os.getpid()}{sys.argv[0]}")
return ( # noqa
f'{datetime.now().strftime("%H:%M:%S.%f")} '
f"\033[38;5;{process_color}m" # color
f"{os.getpid()} "
f"{self._get_process_name()} "
"\033[0m" # end style
f"{style}"
f"%(levelname)s "
f"%(filename)s:%(lineno)d: "
"%(message)s"
"\033[0m" # end style
).replace(" ", " ")
def format(self, record: logging.LogRecord):
"""Overwritten format function."""
# pylint: disable=protected-access
self._style._fmt = self._get_format(record)
return super().format(record)

View File

@ -0,0 +1,172 @@
# -*- 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/>.
"""Logging setup for input-remapper."""
from __future__ import annotations # needed for the TYPE_CHECKING import
import logging
import time
from typing import cast, Type, TYPE_CHECKING, List, Tuple
from evdev.ecodes import EV_ABS, EV_KEY, EV_REL, ABS_HAT0X, ABS_HAT0Y
from inputremapper.input_event import InputEvent
from inputremapper.logging.formatter import ColorfulFormatter
from inputremapper.installation_info import VERSION, COMMIT_HASH
if TYPE_CHECKING:
from inputremapper.injection.mapping_handler import MappingHandler
start = time.time()
class Logger(logging.Logger):
previous_abs_rel_log_time = 0.0
previous_write_debug_log = None
analog_log_threshold = 0.1 # s
def debug_mapping_handler(self, mapping_handler: MappingHandler) -> None:
"""Parse the structure of a mapping_handler and log it."""
if not self.isEnabledFor(logging.DEBUG):
return
lines_and_indent = self._build_mapping_handler_description_tree(mapping_handler)
for line in lines_and_indent:
indent = " "
msg = indent * line[1] + line[0]
self._log(logging.DEBUG, msg, args=())
def write(self, key, uinput) -> None:
"""Log that an event is being written
Parameters
----------
key
anything that can be string formatted, but usually a tuple of
(type, code, value) tuples
"""
# pylint: disable=protected-access
if not self.isEnabledFor(logging.DEBUG):
return
if isinstance(key, InputEvent):
if key.type not in [EV_ABS, EV_REL, EV_KEY]:
return
# Avoid spaming the terminal with tons of high-resolution logs
now = time.time()
if key.type in [EV_ABS, EV_REL] and key.code not in [ABS_HAT0X, ABS_HAT0Y]:
if now - self.previous_abs_rel_log_time < self.analog_log_threshold:
return
self.previous_abs_rel_log_time = now
str_key = repr(key)
str_key = str_key.replace(",)", ")")
msg = f'Writing {str_key} to "{uinput.name}"'
if msg == self.previous_write_debug_log:
# avoid some spam
return
self.previous_write_debug_log = msg
self._log(logging.DEBUG, msg, args=(), stacklevel=2)
def _build_mapping_handler_description_tree(
self,
mapping_handler: MappingHandler,
indent=0,
) -> List[Tuple[str, int]]:
lines_and_indent = [
(str(mapping_handler), indent),
]
mapping_handlers = mapping_handler.get_children()
for sub_handler in mapping_handlers:
sub_list = self._build_mapping_handler_description_tree(
sub_handler, indent + 1
)
lines_and_indent.extend(sub_list)
return lines_and_indent
def is_debug(self) -> bool:
"""True, if the logger is currently in DEBUG mode."""
return self.level <= logging.DEBUG
def log_info(self, name: str = "input-remapper") -> None:
"""Log version and name to the console."""
logger.info(
"%s %s %s https://github.com/sezanzeb/input-remapper",
name,
VERSION,
COMMIT_HASH,
)
if EVDEV_VERSION:
logger.info("python-evdev %s", EVDEV_VERSION)
if self.is_debug():
logger.warning(
"Debug level will log all your keystrokes! Do not post this "
"output in the internet if you typed in sensitive or private "
"information with your device!"
)
def update_verbosity(self, debug: bool) -> None:
"""Set the logging verbosity according to the settings object."""
if debug:
self.setLevel(logging.DEBUG)
else:
self.setLevel(logging.INFO)
for handler in self.handlers:
handler.setFormatter(ColorfulFormatter(debug))
@classmethod
def bootstrap_logger(cls: Type[Logger]) -> Logger:
# https://github.com/python/typeshed/issues/1801
logging.setLoggerClass(cls)
logger = cast(Logger, logging.getLogger("input-remapper"))
handler = logging.StreamHandler()
handler.setFormatter(ColorfulFormatter(False))
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logging.getLogger("asyncio").setLevel(logging.WARNING)
return logger
logger = Logger.bootstrap_logger()
EVDEV_VERSION = None
try:
from importlib.metadata import version
EVDEV_VERSION = version("evdev")
except Exception as error:
logger.info("Could not figure out the evdev version")
logger.debug(error)
# check if the version is something like 1.5.0-beta or 1.5.0-beta.5
IS_BETA = "beta" in VERSION