terminal.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. """ANSI color formatting for output in terminal."""
  2. from __future__ import annotations
  3. import io
  4. import os
  5. import sys
  6. from functools import reduce
  7. from typing import Iterable, Literal
  8. from reflex.utils.decorator import once
  9. _Attribute = Literal[
  10. "bold",
  11. "dark",
  12. "italic",
  13. "underline",
  14. "slow_blink",
  15. "rapid_blink",
  16. "reverse",
  17. "concealed",
  18. "strike",
  19. ]
  20. _ATTRIBUTES: dict[_Attribute, int] = {
  21. "bold": 1,
  22. "dark": 2,
  23. "italic": 3,
  24. "underline": 4,
  25. "slow_blink": 5,
  26. "rapid_blink": 6,
  27. "reverse": 7,
  28. "concealed": 8,
  29. "strike": 9,
  30. }
  31. _Color = Literal[
  32. "black",
  33. "red",
  34. "green",
  35. "yellow",
  36. "blue",
  37. "magenta",
  38. "cyan",
  39. "light_grey",
  40. "dark_grey",
  41. "light_red",
  42. "light_green",
  43. "light_yellow",
  44. "light_blue",
  45. "light_magenta",
  46. "light_cyan",
  47. "white",
  48. ]
  49. _COLORS: dict[_Color, int] = {
  50. "black": 30,
  51. "red": 31,
  52. "green": 32,
  53. "yellow": 33,
  54. "blue": 34,
  55. "magenta": 35,
  56. "cyan": 36,
  57. "light_grey": 37,
  58. "dark_grey": 90,
  59. "light_red": 91,
  60. "light_green": 92,
  61. "light_yellow": 93,
  62. "light_blue": 94,
  63. "light_magenta": 95,
  64. "light_cyan": 96,
  65. "white": 97,
  66. }
  67. _BackgroundColor = Literal[
  68. "on_black",
  69. "on_red",
  70. "on_green",
  71. "on_yellow",
  72. "on_blue",
  73. "on_magenta",
  74. "on_cyan",
  75. "on_light_grey",
  76. "on_dark_grey",
  77. "on_light_red",
  78. "on_light_green",
  79. "on_light_yellow",
  80. "on_light_blue",
  81. "on_light_magenta",
  82. "on_light_cyan",
  83. "on_white",
  84. ]
  85. BACKGROUND_COLORS: dict[_BackgroundColor, int] = {
  86. "on_black": 40,
  87. "on_red": 41,
  88. "on_green": 42,
  89. "on_yellow": 43,
  90. "on_blue": 44,
  91. "on_magenta": 45,
  92. "on_cyan": 46,
  93. "on_light_grey": 47,
  94. "on_dark_grey": 100,
  95. "on_light_red": 101,
  96. "on_light_green": 102,
  97. "on_light_yellow": 103,
  98. "on_light_blue": 104,
  99. "on_light_magenta": 105,
  100. "on_light_cyan": 106,
  101. "on_white": 107,
  102. }
  103. _ANSI_CODES = _ATTRIBUTES | BACKGROUND_COLORS | _COLORS
  104. _RESET_MARKER = "\033[0m"
  105. @once
  106. def _can_colorize() -> bool:
  107. """Check if the output can be colorized.
  108. Copied from _colorize.can_colorize.
  109. https://raw.githubusercontent.com/python/cpython/refs/heads/main/Lib/_colorize.py
  110. Returns:
  111. If the output can be colorized
  112. """
  113. file = sys.stdout
  114. if not sys.flags.ignore_environment:
  115. if os.environ.get("PYTHON_COLORS") == "0":
  116. return False
  117. if os.environ.get("PYTHON_COLORS") == "1":
  118. return True
  119. if os.environ.get("NO_COLOR"):
  120. return False
  121. if os.environ.get("FORCE_COLOR"):
  122. return True
  123. if os.environ.get("TERM") == "dumb":
  124. return False
  125. if not hasattr(file, "fileno"):
  126. return False
  127. if sys.platform == "win32":
  128. try:
  129. import nt
  130. if not nt._supports_virtual_terminal():
  131. return False
  132. except (ImportError, AttributeError):
  133. return False
  134. try:
  135. return os.isatty(file.fileno())
  136. except io.UnsupportedOperation:
  137. return file.isatty()
  138. def _format_str(text: str, ansi_escape_code: int | None) -> str:
  139. """Format text with ANSI escape code.
  140. Args:
  141. text: Text to format
  142. ansi_escape_code: ANSI escape code
  143. Returns:
  144. Formatted text
  145. """
  146. if ansi_escape_code is None:
  147. return text
  148. return f"\033[{ansi_escape_code}m{text}"
  149. def colored(
  150. text: object,
  151. color: _Color | None = None,
  152. background_color: _BackgroundColor | None = None,
  153. attrs: Iterable[_Attribute] = (),
  154. ) -> str:
  155. """Colorize text for terminal output.
  156. Args:
  157. text: Text to colorize
  158. color: Text color
  159. background_color: Background color
  160. attrs: Text attributes
  161. Returns:
  162. Colorized text
  163. """
  164. result = str(text)
  165. if not _can_colorize():
  166. return result
  167. ansi_codes_to_apply = [
  168. _ANSI_CODES.get(x)
  169. for x in [
  170. color,
  171. background_color,
  172. *attrs,
  173. ]
  174. if x
  175. ]
  176. return (
  177. reduce(_format_str, ansi_codes_to_apply, result) + _RESET_MARKER
  178. if ansi_codes_to_apply
  179. else result
  180. )