diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 6e3662a8b7..62c09a14dd 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -18,11 +18,13 @@ import argparse import configparser +import ctypes import fnmatch import os import re import sys import textwrap +from ctypes import wintypes from typing import Any, Dict, List, Match, Optional, Pattern, Sequence, Set, Tuple # autogenerated by setuptools_scm @@ -120,6 +122,10 @@ EX_DATAERR = 65 EX_CONFIG = 78 +# Windows specific constants +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +STD_OUTPUT_HANDLE = wintypes.HANDLE(-11) + # OPTIONS: # # ARGUMENTS: @@ -308,12 +314,44 @@ def _toml_to_parseconfig(toml_dict: Dict[str, Any]) -> Dict[str, Any]: return {k: "" if v is True else v for k, v in toml_dict.items() if v is not False} +def _supports_ansi_colors() -> bool: + if sys.platform == "win32": + # Windows Terminal enables ANSI escape codes by default. In other cases + # it is disabled. + # See https://ss64.com/nt/syntax-ansi.html for more information. + kernel32 = ctypes.WinDLL("kernel32") + + # fmt: off + kernel32.GetConsoleMode.argtypes = ( + wintypes.HANDLE, # _In_ hConsoleHandle + wintypes.LPDWORD, # _Out_ lpMode + ) + # fmt: on + kernel32.GetConsoleMode.restype = wintypes.BOOL + + mode = wintypes.DWORD() + handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)): + # TODO: print a warning with the error message on stderr? + return False + + return (mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 + elif sys.platform == "wasi": + # WASI disables ANSI escape codes for security reasons. + # See https://github.com/WebAssembly/WASI/issues/162. + return False + elif sys.stdout.isatty(): + return True + + return False + + def parse_options( args: Sequence[str], ) -> Tuple[argparse.Namespace, argparse.ArgumentParser, List[str]]: parser = argparse.ArgumentParser(formatter_class=NewlineHelpFormatter) - parser.set_defaults(colors=sys.stdout.isatty()) + parser.set_defaults(colors=_supports_ansi_colors()) parser.add_argument("--version", action="version", version=VERSION) parser.add_argument( @@ -321,8 +359,7 @@ def parse_options( "--disable-colors", action="store_false", dest="colors", - help="disable colors, even when printing to terminal " - "(always set for Windows)", + help="disable colors, even when printing to terminal", ) parser.add_argument( "-c", @@ -1130,7 +1167,7 @@ def main(*args: str) -> int: for dictionary in use_dictionaries: build_dict(dictionary, misspellings, ignore_words) colors = TermColors() - if not options.colors or sys.platform == "win32": + if not options.colors: colors.disable() if options.summary: