33import abc
44import collections
55import colorsys
6+ import enum
67import threading
78from collections import defaultdict
8-
99# Ruff is being stupid and doesn't understand `ClassVar` if it comes from the
1010# `types` module
1111from typing import ClassVar
1212
1313from python_utils import converters , types
1414
15+ from .os_specific import getch
1516from .. import (
1617 base as pbase ,
1718 env ,
1819)
19- from .os_specific import getch
2020
2121ESC = '\x1B '
2222
@@ -178,6 +178,53 @@ def column(self, stream):
178178 return column
179179
180180
181+
182+ class WindowsColors (enum .Enum ):
183+ BLACK = 0 , 0 , 0
184+ BLUE = 0 , 0 , 128
185+ GREEN = 0 , 128 , 0
186+ CYAN = 0 , 128 , 128
187+ RED = 128 , 0 , 0
188+ MAGENTA = 128 , 0 , 128
189+ YELLOW = 128 , 128 , 0
190+ GREY = 192 , 192 , 192
191+ INTENSE_BLACK = 128 , 128 , 128
192+ INTENSE_BLUE = 0 , 0 , 255
193+ INTENSE_GREEN = 0 , 255 , 0
194+ INTENSE_CYAN = 0 , 255 , 255
195+ INTENSE_RED = 255 , 0 , 0
196+ INTENSE_MAGENTA = 255 , 0 , 255
197+ INTENSE_YELLOW = 255 , 255 , 0
198+ INTENSE_WHITE = 255 , 255 , 255
199+
200+ @staticmethod
201+ def from_rgb (rgb : types .Tuple [int , int , int ]):
202+ """Find the closest ConsoleColor to the given RGB color."""
203+
204+ def color_distance (rgb1 , rgb2 ):
205+ return sum ((c1 - c2 ) ** 2 for c1 , c2 in zip (rgb1 , rgb2 ))
206+
207+ return min (
208+ WindowsColors ,
209+ key = lambda color : color_distance (color .value , rgb ),
210+ )
211+
212+
213+ class WindowsColor :
214+ __slots__ = 'color' ,
215+
216+ def __init__ (self , color : Color ):
217+ self .color = color
218+
219+ def __call__ (self , text ):
220+ return text
221+ # In the future we might want to use this, but it requires direct printing to stdout and all of our surrounding functions expect buffered output so it's not feasible right now.
222+ # Additionally, recent Windows versions all support ANSI codes without issue so there is little need.
223+ # from progressbar.terminal.os_specific import windows
224+ # windows.print_color(text, WindowsColors.from_rgb(self.color.rgb))
225+
226+
227+
181228class RGB (collections .namedtuple ('RGB' , ['red' , 'green' , 'blue' ])):
182229 __slots__ = ()
183230
@@ -207,6 +254,14 @@ def to_ansi_256(self):
207254 blue = round (self .blue / 255 * 5 )
208255 return 16 + 36 * red + 6 * green + blue
209256
257+ @property
258+ def to_windows (self ):
259+ '''
260+ Convert an RGB color (0-255 per channel) to the closest color in the
261+ Windows 16 color scheme.
262+ '''
263+ return WindowsColors .from_rgb ((self .red , self .green , self .blue ))
264+
210265 def interpolate (self , end : RGB , step : float ) -> RGB :
211266 return RGB (
212267 int (self .red + (end .red - self .red ) * step ),
@@ -286,27 +341,36 @@ def __call__(self, value: str) -> str:
286341
287342 @property
288343 def fg (self ):
289- return SGRColor (self , 38 , 39 )
344+ if env .COLOR_SUPPORT is env .ColorSupport .WINDOWS :
345+ return WindowsColor (self )
346+ else :
347+ return SGRColor (self , 38 , 39 )
290348
291349 @property
292350 def bg (self ):
293- return SGRColor (self , 48 , 49 )
351+ if env .COLOR_SUPPORT is env .ColorSupport .WINDOWS :
352+ return DummyColor ()
353+ else :
354+ return SGRColor (self , 48 , 49 )
294355
295356 @property
296357 def underline (self ):
297- return SGRColor (self , 58 , 59 )
358+ if env .COLOR_SUPPORT is env .ColorSupport .WINDOWS :
359+ return DummyColor ()
360+ else :
361+ return SGRColor (self , 58 , 59 )
298362
299363 @property
300364 def ansi (self ) -> types .Optional [str ]:
301365 if (
302- env .COLOR_SUPPORT is env .ColorSupport .XTERM_TRUECOLOR
366+ env .COLOR_SUPPORT is env .ColorSupport .XTERM_TRUECOLOR
303367 ): # pragma: no branch
304368 return f'2;{ self .rgb .red } ;{ self .rgb .green } ;{ self .rgb .blue } '
305369
306370 if self .xterm : # pragma: no branch
307371 color = self .xterm
308372 elif (
309- env .COLOR_SUPPORT is env .ColorSupport .XTERM_256
373+ env .COLOR_SUPPORT is env .ColorSupport .XTERM_256
310374 ): # pragma: no branch
311375 color = self .rgb .to_ansi_256
312376 elif env .COLOR_SUPPORT is env .ColorSupport .XTERM : # pragma: no branch
@@ -354,11 +418,11 @@ class Colors:
354418
355419 @classmethod
356420 def register (
357- cls ,
358- rgb : RGB ,
359- hls : types .Optional [HSL ] = None ,
360- name : types .Optional [str ] = None ,
361- xterm : types .Optional [int ] = None ,
421+ cls ,
422+ rgb : RGB ,
423+ hls : types .Optional [HSL ] = None ,
424+ name : types .Optional [str ] = None ,
425+ xterm : types .Optional [int ] = None ,
362426 ) -> Color :
363427 color = Color (rgb , hls , name , xterm )
364428
@@ -395,9 +459,9 @@ def __call__(self, value: float) -> Color:
395459 def get_color (self , value : float ) -> Color :
396460 'Map a value from 0 to 1 to a color.'
397461 if (
398- value == pbase .Undefined
399- or value == pbase .UnknownLength
400- or value <= 0
462+ value == pbase .Undefined
463+ or value == pbase .UnknownLength
464+ or value <= 0
401465 ):
402466 return self .colors [0 ]
403467 elif value >= 1 :
@@ -443,14 +507,14 @@ def get_color(value: float, color: OptionalColor) -> Color | None:
443507
444508
445509def apply_colors (
446- text : str ,
447- percentage : float | None = None ,
448- * ,
449- fg : OptionalColor = None ,
450- bg : OptionalColor = None ,
451- fg_none : Color | None = None ,
452- bg_none : Color | None = None ,
453- ** kwargs : types .Any ,
510+ text : str ,
511+ percentage : float | None = None ,
512+ * ,
513+ fg : OptionalColor = None ,
514+ bg : OptionalColor = None ,
515+ fg_none : Color | None = None ,
516+ bg_none : Color | None = None ,
517+ ** kwargs : types .Any ,
454518) -> str :
455519 '''Apply colors/gradients to a string depending on the given percentage.
456520
@@ -475,6 +539,17 @@ def apply_colors(
475539 return text
476540
477541
542+ class DummyColor :
543+ def __call__ (self , text ):
544+ return text
545+
546+ def __getattr__ (self , item ):
547+ return self
548+
549+ def __repr__ (self ):
550+ return 'DummyColor()'
551+
552+
478553class SGR (CSI ):
479554 _start_code : int
480555 _end_code : int
0 commit comments