Files
talkscore-asr/app/console.py
T
Vladimir BryzgalovandClaude Opus 5 ec0e84935e Читаемый вывод в консоли: цвета и перенос строк
Windows-консоль не разбирала ANSI и печатала их как мусор, поэтому цвет
был выключен целиком. Теперь разбор включается через WinAPI, а при неудаче
цвет молча отключается. Длинные сообщения переносятся по словам с отступом
под текст. Вывод Caddy уведён в файл - он забивал консоль сертификатами.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 16:56:58 +05:00

108 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Читаемый вывод в консоли: цвета и перенос длинных строк.
Windows-консоль по умолчанию не разбирает ANSI-последовательности и печатает
их как мусор вида `[34mINFO[0m`. Начиная с Windows 10 разбор можно включить
через WinAPI - это и делается ниже. Если включить не удалось (старая система,
вывод перенаправлен в файл), цвета молча отключаются: лучше блёклый лог,
чем лог с мусором.
"""
import logging
import os
import shutil
import sys
import textwrap
from collections.abc import Callable
__all__ = ["enable_ansi", "ConsoleFormatter", "setup_logging"]
# Цвет только там, где он несёт смысл: обычные сообщения остаются обычными,
# чтобы предупреждения и ошибки было видно с одного взгляда.
COLORS = {
"DEBUG": "\033[90m",
"INFO": "",
"WARNING": "\033[33m",
"ERROR": "\033[31m",
"CRITICAL": "\033[1;31m",
}
DIM = "\033[90m"
RESET = "\033[0m"
MIN_WIDTH = 60
FALLBACK_WIDTH = 100
def enable_ansi() -> bool:
"""Включает разбор ANSI в консоли Windows. Возвращает, можно ли красить."""
if not sys.stdout.isatty():
return False
if os.name != "nt":
return True
try:
import ctypes
kernel32 = ctypes.windll.kernel32
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
mode = ctypes.c_uint32()
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
return False
# ENABLE_VIRTUAL_TERMINAL_PROCESSING
return bool(kernel32.SetConsoleMode(handle, mode.value | 0x0004))
except (ImportError, AttributeError, OSError):
return False
def console_width() -> int:
"""Ширина окна консоли. При перенаправлении в файл берём разумную по умолчанию."""
width = shutil.get_terminal_size((FALLBACK_WIDTH, 24)).columns
return max(MIN_WIDTH, width)
class ConsoleFormatter(logging.Formatter):
"""Формат вида `09:29:46 WARNING текст`, длинные строки переносятся по словам.
Продолжение переносится с отступом под текст сообщения, а не под начало
строки: так видно, где кончается одно сообщение и начинается следующее.
"""
def __init__(self, colored: bool,
width: Callable[[], int] = console_width) -> None:
super().__init__(datefmt="%H:%M:%S")
self.colored = colored
# Ширину берём через переданную функцию, а не из глобальной: окно консоли
# меняется на ходу, а в тестах ширину надо задавать явно.
self.width = width
def format(self, record: logging.LogRecord) -> str:
stamp = self.formatTime(record, self.datefmt)
level = record.levelname
head = f"{stamp} {level:<8}"
indent = " " * len(head)
body = record.getMessage()
if record.exc_info:
body = f"{body}\n{self.formatException(record.exc_info)}"
lines: list[str] = []
room = self.width() - len(head)
for chunk in body.splitlines() or [""]:
lines.extend(textwrap.wrap(chunk, width=room) or [""])
if not self.colored:
return "\n".join([head + lines[0]]
+ [indent + line for line in lines[1:]])
tint = COLORS.get(level, "")
painted = f"{DIM}{stamp}{RESET} {tint}{level:<8}{RESET}" if tint \
else f"{DIM}{stamp}{RESET} {level:<8}"
return "\n".join([painted + lines[0]]
+ [indent + line for line in lines[1:]])
def setup_logging(level: int = logging.INFO) -> bool:
"""Настраивает единый обработчик вывода. Возвращает, включены ли цвета."""
colored = enable_ansi()
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(ConsoleFormatter(colored))
root = logging.getLogger()
root.handlers[:] = [handler]
root.setLevel(level)
return colored