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:
0
inputremapper/logging/__init__.py
Normal file
0
inputremapper/logging/__init__.py
Normal file
143
inputremapper/logging/formatter.py
Normal file
143
inputremapper/logging/formatter.py
Normal 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)
|
||||
172
inputremapper/logging/logger.py
Normal file
172
inputremapper/logging/logger.py
Normal 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
|
||||
Reference in New Issue
Block a user