Skip to content

Commit 52c286f

Browse files
authored
Enable output color and effects on Windows (#7463)
This uses VT100 terminal processing available in Windows 10 with build >10586 (released November 2015).
1 parent d4151a8 commit 52c286f

File tree

1 file changed

+56
-14
lines changed

1 file changed

+56
-14
lines changed

mypy/util.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
# error location when printing source code snippet.
3939
MINIMUM_WIDTH = 20
4040

41+
# VT100 color code processing was added in Windows 10, but only the second major update,
42+
# Threshold 2. Fortunately, everyone (even on LTSB, Long Term Support Branch) should
43+
# have a version of Windows 10 newer than this. Note that Windows 8 and below are not
44+
# supported, but are either going out of support, or make up only a few % of the market.
45+
MINIMUM_WINDOWS_MAJOR_VT100 = 10
46+
MINIMUM_WINDOWS_BUILD_VT100 = 10586
47+
4148
default_python2_interpreter = \
4249
['python2', 'python', '/usr/bin/python', 'C:\\Python27\\python.exe'] # type: Final
4350

@@ -467,30 +474,68 @@ class FancyFormatter:
467474
def __init__(self, f_out: IO[str], f_err: IO[str], show_error_codes: bool) -> None:
468475
self.show_error_codes = show_error_codes
469476
# Check if we are in a human-facing terminal on a supported platform.
470-
if sys.platform not in ('linux', 'darwin'):
477+
if sys.platform not in ('linux', 'darwin', 'win32'):
471478
self.dummy_term = True
472479
return
473480
force_color = int(os.getenv('MYPY_FORCE_COLOR', '0'))
474481
if not force_color and (not f_out.isatty() or not f_err.isatty()):
475482
self.dummy_term = True
476483
return
477-
478-
# We in a human-facing terminal, check if it supports enough styling.
484+
if sys.platform == 'win32':
485+
self.dummy_term = not self.initialize_win_colors()
486+
else:
487+
self.dummy_term = not self.initialize_unix_colors()
488+
if not self.dummy_term:
489+
self.colors = {'red': self.RED, 'green': self.GREEN,
490+
'blue': self.BLUE, 'yellow': self.YELLOW,
491+
'none': ''}
492+
493+
def initialize_win_colors(self) -> bool:
494+
"""Return True if initialization was successful and we can use colors, False otherwise"""
495+
# Windows ANSI escape sequences are only supported on Threshold 2 and above.
496+
# we check with an assert at runtime and an if check for mypy, as asserts do not
497+
# yet narrow platform
498+
assert sys.platform == 'win32'
499+
if sys.platform == 'win32':
500+
winver = sys.getwindowsversion()
501+
if (winver.major < MINIMUM_WINDOWS_MAJOR_VT100
502+
or winver.build < MINIMUM_WINDOWS_BUILD_VT100):
503+
return False
504+
import ctypes
505+
kernel32 = ctypes.windll.kernel32
506+
ENABLE_PROCESSED_OUTPUT = 0x1
507+
ENABLE_WRAP_AT_EOL_OUTPUT = 0x2
508+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x4
509+
STD_OUTPUT_HANDLE = -11
510+
kernel32.SetConsoleMode(kernel32.GetStdHandle(STD_OUTPUT_HANDLE),
511+
ENABLE_PROCESSED_OUTPUT
512+
| ENABLE_WRAP_AT_EOL_OUTPUT
513+
| ENABLE_VIRTUAL_TERMINAL_PROCESSING)
514+
self.BOLD = '\033[1m'
515+
self.UNDER = '\033[4m'
516+
self.BLUE = '\033[94m'
517+
self.GREEN = '\033[92m'
518+
self.RED = '\033[91m'
519+
self.YELLOW = '\033[93m'
520+
self.NORMAL = '\033[0m'
521+
self.DIM = '\033[2m'
522+
return True
523+
return False
524+
525+
def initialize_unix_colors(self) -> bool:
526+
"""Return True if initialization was successful and we can use colors, False otherwise"""
479527
if not CURSES_ENABLED:
480-
self.dummy_term = True
481-
return
528+
return False
482529
try:
483530
curses.setupterm()
484531
except curses.error:
485532
# Most likely terminfo not found.
486-
self.dummy_term = True
487-
return
533+
return False
488534
bold = curses.tigetstr('bold')
489535
under = curses.tigetstr('smul')
490536
set_color = curses.tigetstr('setaf')
491-
self.dummy_term = not (bold and under and set_color)
492-
if self.dummy_term:
493-
return
537+
if not (bold and under and set_color):
538+
return False
494539

495540
self.NORMAL = curses.tigetstr('sgr0').decode()
496541
self.BOLD = bold.decode()
@@ -503,10 +548,7 @@ def __init__(self, f_out: IO[str], f_err: IO[str], show_error_codes: bool) -> No
503548
self.GREEN = curses.tparm(set_color, curses.COLOR_GREEN).decode()
504549
self.RED = curses.tparm(set_color, curses.COLOR_RED).decode()
505550
self.YELLOW = curses.tparm(set_color, curses.COLOR_YELLOW).decode()
506-
507-
self.colors = {'red': self.RED, 'green': self.GREEN,
508-
'blue': self.BLUE, 'yellow': self.YELLOW,
509-
'none': ''}
551+
return True
510552

511553
def style(self, text: str, color: Literal['red', 'green', 'blue', 'yellow', 'none'],
512554
bold: bool = False, underline: bool = False, dim: bool = False) -> str:

0 commit comments

Comments
 (0)