File: formatter.py

package info (click to toggle)
input-remapper 2.1.1-1%2Bdeb13u1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 2,860 kB
  • sloc: python: 27,277; sh: 191; xml: 33; makefile: 3
file content (143 lines) | stat: -rw-r--r-- 5,224 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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)