from __future__ import annotations import os import sys from enum import Enum from typing import Final # Colors are disabled in non-TTY environments such as pipes. This means if output is redirected # to a file, it won't contain color codes. Colors are always enabled on continuous integration. IS_CI: Final[bool] = bool(os.environ.get("CI")) STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty()) STDERR_TTY: Final[bool] = bool(sys.stderr.isatty()) def _color_supported(stdout: bool) -> bool: """ Validates if the current environment supports colored output. Attempts to enable ANSI escape code support on Windows 10 and later. """ if IS_CI: return True if sys.platform != "win32": return STDOUT_TTY if stdout else STDERR_TTY else: from ctypes import POINTER, WINFUNCTYPE, WinError, windll from ctypes.wintypes import BOOL, DWORD, HANDLE STD_HANDLE = -11 if stdout else -12 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 def err_handler(result, func, args): if not result: raise WinError() return args GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),)) GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( ("GetConsoleMode", windll.kernel32), ((1, "hConsoleHandle"), (2, "lpMode")), ) GetConsoleMode.errcheck = err_handler SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( ("SetConsoleMode", windll.kernel32), ((1, "hConsoleHandle"), (1, "dwMode")), ) SetConsoleMode.errcheck = err_handler try: handle = GetStdHandle(STD_HANDLE) flags = GetConsoleMode(handle) SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) return True except OSError: return False STDOUT_COLOR: Final[bool] = _color_supported(True) STDERR_COLOR: Final[bool] = _color_supported(False) _stdout_override: bool = STDOUT_COLOR _stderr_override: bool = STDERR_COLOR def toggle_color(stdout: bool, value: bool | None = None) -> None: """ Explicitly toggle color codes, regardless of support. - `stdout`: A boolean to choose the output stream. `True` for stdout, `False` for stderr. - `value`: An optional boolean to explicitly set the color state instead of toggling. """ if stdout: global _stdout_override _stdout_override = value if value is not None else not _stdout_override else: global _stderr_override _stderr_override = value if value is not None else not _stderr_override class Ansi(Enum): """ Enum class for adding ansi codepoints directly into strings. Automatically converts values to strings representing their internal value. """ RESET = "\x1b[0m" BOLD = "\x1b[1m" DIM = "\x1b[2m" ITALIC = "\x1b[3m" UNDERLINE = "\x1b[4m" STRIKETHROUGH = "\x1b[9m" REGULAR = "\x1b[22;23;24;29m" BLACK = "\x1b[30m" RED = "\x1b[31m" GREEN = "\x1b[32m" YELLOW = "\x1b[33m" BLUE = "\x1b[34m" MAGENTA = "\x1b[35m" CYAN = "\x1b[36m" WHITE = "\x1b[37m" GRAY = "\x1b[90m" def __str__(self) -> str: return self.value def print_info(*values: object) -> None: """Prints a informational message with formatting.""" if _stdout_override: print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values, Ansi.RESET) else: print("INFO:", *values) def print_warning(*values: object) -> None: """Prints a warning message with formatting.""" if _stderr_override: print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr) else: print("WARNING:", *values, file=sys.stderr) def print_error(*values: object) -> None: """Prints an error message with formatting.""" if _stderr_override: print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr) else: print("ERROR:", *values, file=sys.stderr)