Greatly improved windows color support and fixed #291 · commit-0/python-progressbar@a9c6770 · GitHub
Skip to content

Commit a9c6770

Browse files
committed
Greatly improved windows color support and fixed wolph#291
1 parent 320bb54 commit a9c6770

4 files changed

Lines changed: 184 additions & 46 deletions

File tree

progressbar/env.py

Lines changed: 34 additions & 9 deletions

progressbar/terminal/base.py

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
import abc
44
import collections
55
import colorsys
6+
import enum
67
import threading
78
from collections import defaultdict
8-
99
# Ruff is being stupid and doesn't understand `ClassVar` if it comes from the
1010
# `types` module
1111
from typing import ClassVar
1212

1313
from python_utils import converters, types
1414

15+
from .os_specific import getch
1516
from .. import (
1617
base as pbase,
1718
env,
1819
)
19-
from .os_specific import getch
2020

2121
ESC = '\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+
181228
class 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

445509
def 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+
478553
class SGR(CSI):
479554
_start_code: int
480555
_end_code: int

progressbar/terminal/os_specific/__init__.py

Lines changed: 5 additions & 0 deletions

0 commit comments

Comments
 (0)