5555
5656
5757class Color :
58- """Utility class for terminal color formatting"""
58+ """Utility class for terminal color formatting.
59+
60+ ANSI codes are emitted based on the destination stream and environment:
61+ - NO_COLOR=<non-empty> always strip colors (no-color.org)
62+ - FORCE_COLOR=<non-empty> always emit colors
63+ - otherwise: emit only when the destination stream is a TTY
64+
65+ Callers that write to stderr should pass stream=sys.stderr so the TTY
66+ check targets the correct destination.
67+ """
5968
6069 # ANSI color codes
6170 RED = "\033 [31m"
@@ -70,8 +79,23 @@ class Color:
7079 RESET = "\033 [0m"
7180
7281 @staticmethod
73- def format (text : str , color : str , bold : bool = False ) -> str :
74- """Format text with color and optional bold attribute"""
82+ def _should_color (stream = None ) -> bool :
83+ if os .environ .get ("NO_COLOR" ):
84+ return False
85+ if os .environ .get ("FORCE_COLOR" ):
86+ return True
87+ stream = stream or sys .stdout
88+ return hasattr (stream , "isatty" ) and stream .isatty ()
89+
90+ @staticmethod
91+ def format (text : str , color : str , bold : bool = False , stream = None ) -> str :
92+ """Format text with color and optional bold attribute.
93+
94+ Returns plain text (no ANSI codes) when the destination stream is not
95+ a TTY, or when NO_COLOR is set. Set FORCE_COLOR to override.
96+ """
97+ if not Color ._should_color (stream ):
98+ return text
7599 result = color
76100 if bold :
77101 result += Color .BOLD
@@ -81,8 +105,8 @@ def format(text: str, color: str, bold: bool = False) -> str:
81105 def _create_color_method (color_code : str ):
82106 """Create a color method for the given color code"""
83107
84- def color_method (text : str , bold : bool = False ) -> str :
85- return Color .format (text , color_code , bold )
108+ def color_method (text : str , bold : bool = False , stream = None ) -> str :
109+ return Color .format (text , color_code , bold , stream = stream )
86110
87111 return color_method
88112
@@ -148,16 +172,19 @@ def check_skip_builds(args) -> Tuple[bool, bool]:
148172
149173def fatal (message : str ) -> None :
150174 """Print fatal error and exit with backtrace"""
175+ err = sys .stderr
151176 print (
152- f"{ Color .red (get_timestamp ())} { Color .red ('[FATAL]' , bold = True )} { message } " , file = sys .stderr
177+ f"{ Color .red (get_timestamp (), stream = err )} "
178+ f"{ Color .red ('[FATAL]' , bold = True , stream = err )} { message } " ,
179+ file = err ,
153180 )
154- print ("\n Backtrace: ..." , file = sys . stderr )
155- traceback .print_list (traceback .extract_stack ()[- 3 :], file = sys . stderr )
181+ print ("\n Backtrace: ..." , file = err )
182+ traceback .print_list (traceback .extract_stack ()[- 3 :], file = err )
156183 sys .exit (1 )
157184
158185
159186def warn (message : str ) -> None :
160- print (f"{ Color .yellow ('WARNING:' )} { message } " , file = sys .stderr )
187+ print (f"{ Color .yellow ('WARNING:' , stream = sys . stderr )} { message } " , file = sys .stderr )
161188
162189
163190def _get_holohub_root () -> Path :
0 commit comments