From cf90841789fc33bf10497a90e1ffecd417651802 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 30 Jun 2026 04:28:33 +0200 Subject: [PATCH] feat(log_system/logger): enable Windows console ANSI colors and fix color formatting --- core/log_system/logger.py | 66 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/core/log_system/logger.py b/core/log_system/logger.py index c0a5d49..92efed0 100644 --- a/core/log_system/logger.py +++ b/core/log_system/logger.py @@ -74,6 +74,57 @@ DEFAULT_CONFIG = { } +_WINDOWS_VT_PROCESSING_ENABLED = False + + +def _enable_windows_virtual_terminal_processing(): + """Enable ANSI escape sequence handling for classic Windows consoles.""" + global _WINDOWS_VT_PROCESSING_ENABLED + if _WINDOWS_VT_PROCESSING_ENABLED or os.name != "nt": + return + + try: + import ctypes + + enable_virtual_terminal_processing = 0x0004 + std_handles = (-11, -12) + invalid_handle_value = ctypes.c_void_p(-1).value + + kernel32 = ctypes.windll.kernel32 + kernel32.GetStdHandle.restype = ctypes.c_void_p + enabled_any = False + for std_handle in std_handles: + handle = kernel32.GetStdHandle(std_handle) + if handle in (None, 0, invalid_handle_value): + continue + + mode = ctypes.c_uint32() + if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)): + continue + + new_mode = mode.value | enable_virtual_terminal_processing + if new_mode != mode.value: + kernel32.SetConsoleMode(handle, new_mode) + + enabled_any = True + + _WINDOWS_VT_PROCESSING_ENABLED = enabled_any + except Exception: + return + + +def _get_console_output_stream(): + """Return the console stream that preserves ANSI colors under wrapped launchers.""" + for stream_name in ("__stdout__", "__stderr__", "stdout", "stderr"): + stream = getattr(sys, stream_name, None) + if stream is None: + continue + if getattr(stream, "closed", False): + continue + return stream + return None + + class ColoredFormatter(logging.Formatter): """Formatter that adds colors to console logs""" @@ -165,13 +216,13 @@ class ColoredFormatter(logging.Formatter): if not rgb: return text r, g, b = rgb - return f"\033[1m\033[48;2;{r};{g};{b};97m {text} {COLORS['RESET']}" + return f"\033[1;97;48;2;{r};{g};{b}m {text} {COLORS['RESET']}" def _color_timestamp(self, text): - return f"{COLORS['TIME_BG']} {text} {COLORS['RESET']}" + return f"\033[97;48;2;38;63;76m {text} {COLORS['RESET']}" def _color_root_label(self, text): - return f"{COLORS['TIME_BG']} {text} {COLORS['RESET']}" + return f"\033[97;48;2;38;63;76m {text} {COLORS['RESET']}" def _color_separator(self, levelname, text): rgb = LEVEL_THEME.get(levelname) @@ -266,6 +317,9 @@ class AzLogsLogger: """Configure the logger""" self.config.update(config) + if self.config.get("use_colors"): + _enable_windows_virtual_terminal_processing() + # If file logging is enabled, ensure the directory exists if self.config.get("log_to_file") and self.config.get("log_dir"): try: @@ -381,7 +435,11 @@ class AzLogsLogger: logger.propagate = False # Add console handler - console_handler = logging.StreamHandler(sys.stdout) + if self.config["use_colors"]: + _enable_windows_virtual_terminal_processing() + + console_stream = _get_console_output_stream() + console_handler = logging.StreamHandler(console_stream) console_formatter = ColoredFormatter( fmt="[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", datefmt=self.config["timestamp_format"],