color.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. from __future__ import annotations
  2. import os
  3. import sys
  4. from enum import Enum
  5. from typing import Final
  6. # Colors are disabled in non-TTY environments such as pipes. This means if output is redirected
  7. # to a file, it won't contain color codes. Colors are always enabled on continuous integration.
  8. IS_CI: Final[bool] = bool(os.environ.get("CI"))
  9. STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty())
  10. STDERR_TTY: Final[bool] = bool(sys.stderr.isatty())
  11. def _color_supported(stdout: bool) -> bool:
  12. """
  13. Validates if the current environment supports colored output. Attempts to enable ANSI escape
  14. code support on Windows 10 and later.
  15. """
  16. if IS_CI:
  17. return True
  18. if sys.platform != "win32":
  19. return STDOUT_TTY if stdout else STDERR_TTY
  20. else:
  21. from ctypes import POINTER, WINFUNCTYPE, WinError, windll
  22. from ctypes.wintypes import BOOL, DWORD, HANDLE
  23. STD_HANDLE = -11 if stdout else -12
  24. ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
  25. def err_handler(result, func, args):
  26. if not result:
  27. raise WinError()
  28. return args
  29. GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),))
  30. GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
  31. ("GetConsoleMode", windll.kernel32),
  32. ((1, "hConsoleHandle"), (2, "lpMode")),
  33. )
  34. GetConsoleMode.errcheck = err_handler
  35. SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
  36. ("SetConsoleMode", windll.kernel32),
  37. ((1, "hConsoleHandle"), (1, "dwMode")),
  38. )
  39. SetConsoleMode.errcheck = err_handler
  40. try:
  41. handle = GetStdHandle(STD_HANDLE)
  42. flags = GetConsoleMode(handle)
  43. SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
  44. return True
  45. except OSError:
  46. return False
  47. STDOUT_COLOR: Final[bool] = _color_supported(True)
  48. STDERR_COLOR: Final[bool] = _color_supported(False)
  49. _stdout_override: bool = STDOUT_COLOR
  50. _stderr_override: bool = STDERR_COLOR
  51. def toggle_color(stdout: bool, value: bool | None = None) -> None:
  52. """
  53. Explicitly toggle color codes, regardless of support.
  54. - `stdout`: A boolean to choose the output stream. `True` for stdout, `False` for stderr.
  55. - `value`: An optional boolean to explicitly set the color state instead of toggling.
  56. """
  57. if stdout:
  58. global _stdout_override
  59. _stdout_override = value if value is not None else not _stdout_override
  60. else:
  61. global _stderr_override
  62. _stderr_override = value if value is not None else not _stderr_override
  63. class Ansi(Enum):
  64. """
  65. Enum class for adding ansi codepoints directly into strings. Automatically converts values to
  66. strings representing their internal value.
  67. """
  68. RESET = "\x1b[0m"
  69. BOLD = "\x1b[1m"
  70. DIM = "\x1b[2m"
  71. ITALIC = "\x1b[3m"
  72. UNDERLINE = "\x1b[4m"
  73. STRIKETHROUGH = "\x1b[9m"
  74. REGULAR = "\x1b[22;23;24;29m"
  75. BLACK = "\x1b[30m"
  76. RED = "\x1b[31m"
  77. GREEN = "\x1b[32m"
  78. YELLOW = "\x1b[33m"
  79. BLUE = "\x1b[34m"
  80. MAGENTA = "\x1b[35m"
  81. CYAN = "\x1b[36m"
  82. WHITE = "\x1b[37m"
  83. GRAY = "\x1b[90m"
  84. def __str__(self) -> str:
  85. return self.value
  86. def print_info(*values: object) -> None:
  87. """Prints a informational message with formatting."""
  88. if _stdout_override:
  89. print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values, Ansi.RESET)
  90. else:
  91. print("INFO:", *values)
  92. def print_warning(*values: object) -> None:
  93. """Prints a warning message with formatting."""
  94. if _stderr_override:
  95. print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
  96. else:
  97. print("WARNING:", *values, file=sys.stderr)
  98. def print_error(*values: object) -> None:
  99. """Prints an error message with formatting."""
  100. if _stderr_override:
  101. print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
  102. else:
  103. print("ERROR:", *values, file=sys.stderr)