From 896739353a613f23c007d9acaa2809010a522a37 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 16 Sep 2022 11:10:14 +0200 Subject: Adding upstream version 2.2.0. Signed-off-by: Daniel Baumann --- colorclass/windows.py | 388 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 colorclass/windows.py (limited to 'colorclass/windows.py') diff --git a/colorclass/windows.py b/colorclass/windows.py new file mode 100644 index 0000000..8f69478 --- /dev/null +++ b/colorclass/windows.py @@ -0,0 +1,388 @@ +"""Windows console screen buffer handlers.""" + +from __future__ import print_function + +import atexit +import ctypes +import re +import sys + +from colorclass.codes import ANSICodeMapping, BASE_CODES +from colorclass.core import RE_SPLIT + +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 +INVALID_HANDLE_VALUE = -1 +IS_WINDOWS = sys.platform == 'win32' +RE_NUMBER_SEARCH = re.compile(r'\033\[([\d;]+)m') +STD_ERROR_HANDLE = -12 +STD_OUTPUT_HANDLE = -11 +WINDOWS_CODES = { + '/all': -33, '/fg': -39, '/bg': -49, + + 'black': 0, 'red': 4, 'green': 2, 'yellow': 6, 'blue': 1, 'magenta': 5, 'cyan': 3, 'white': 7, + + 'bgblack': -8, 'bgred': 64, 'bggreen': 32, 'bgyellow': 96, 'bgblue': 16, 'bgmagenta': 80, 'bgcyan': 48, + 'bgwhite': 112, + + 'hiblack': 8, 'hired': 12, 'higreen': 10, 'hiyellow': 14, 'hiblue': 9, 'himagenta': 13, 'hicyan': 11, 'hiwhite': 15, + + 'hibgblack': 128, 'hibgred': 192, 'hibggreen': 160, 'hibgyellow': 224, 'hibgblue': 144, 'hibgmagenta': 208, + 'hibgcyan': 176, 'hibgwhite': 240, + + '/black': -39, '/red': -39, '/green': -39, '/yellow': -39, '/blue': -39, '/magenta': -39, '/cyan': -39, + '/white': -39, '/hiblack': -39, '/hired': -39, '/higreen': -39, '/hiyellow': -39, '/hiblue': -39, '/himagenta': -39, + '/hicyan': -39, '/hiwhite': -39, + + '/bgblack': -49, '/bgred': -49, '/bggreen': -49, '/bgyellow': -49, '/bgblue': -49, '/bgmagenta': -49, + '/bgcyan': -49, '/bgwhite': -49, '/hibgblack': -49, '/hibgred': -49, '/hibggreen': -49, '/hibgyellow': -49, + '/hibgblue': -49, '/hibgmagenta': -49, '/hibgcyan': -49, '/hibgwhite': -49, +} + + +class COORD(ctypes.Structure): + """COORD structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119.""" + + _fields_ = [ + ('X', ctypes.c_short), + ('Y', ctypes.c_short), + ] + + +class SmallRECT(ctypes.Structure): + """SMALL_RECT structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311.""" + + _fields_ = [ + ('Left', ctypes.c_short), + ('Top', ctypes.c_short), + ('Right', ctypes.c_short), + ('Bottom', ctypes.c_short), + ] + + +class ConsoleScreenBufferInfo(ctypes.Structure): + """CONSOLE_SCREEN_BUFFER_INFO structure. http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093.""" + + _fields_ = [ + ('dwSize', COORD), + ('dwCursorPosition', COORD), + ('wAttributes', ctypes.c_ushort), + ('srWindow', SmallRECT), + ('dwMaximumWindowSize', COORD) + ] + + +def init_kernel32(kernel32=None): + """Load a unique instance of WinDLL into memory, set arg/return types, and get stdout/err handles. + + 1. Since we are setting DLL function argument types and return types, we need to maintain our own instance of + kernel32 to prevent overriding (or being overwritten by) user's own changes to ctypes.windll.kernel32. + 2. While we're doing all this we might as well get the handles to STDOUT and STDERR streams. + 3. If either stream has already been replaced set return value to INVALID_HANDLE_VALUE to indicate it shouldn't be + replaced. + + :raise AttributeError: When called on a non-Windows platform. + + :param kernel32: Optional mock kernel32 object. For testing. + + :return: Loaded kernel32 instance, stderr handle (int), stdout handle (int). + :rtype: tuple + """ + if not kernel32: + kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32 # Load our own instance. Unique memory address. + kernel32.GetStdHandle.argtypes = [ctypes.c_ulong] + kernel32.GetStdHandle.restype = ctypes.c_void_p + kernel32.GetConsoleScreenBufferInfo.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(ConsoleScreenBufferInfo), + ] + kernel32.GetConsoleScreenBufferInfo.restype = ctypes.c_long + + # Get handles. + if hasattr(sys.stderr, '_original_stream'): + stderr = INVALID_HANDLE_VALUE + else: + stderr = kernel32.GetStdHandle(STD_ERROR_HANDLE) + if hasattr(sys.stdout, '_original_stream'): + stdout = INVALID_HANDLE_VALUE + else: + stdout = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + + return kernel32, stderr, stdout + + +def get_console_info(kernel32, handle): + """Get information about this current console window. + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231 + https://code.google.com/p/colorama/issues/detail?id=47 + https://bitbucket.org/pytest-dev/py/src/4617fe46/py/_io/terminalwriter.py + + Windows 10 Insider since around February 2016 finally introduced support for ANSI colors. No need to replace stdout + and stderr streams to intercept colors and issue multiple SetConsoleTextAttribute() calls for these consoles. + + :raise OSError: When GetConsoleScreenBufferInfo or GetConsoleMode API calls fail. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int handle: stderr or stdout handle. + + :return: Foreground and background colors (integers) as well as native ANSI support (bool). + :rtype: tuple + """ + # Query Win32 API. + csbi = ConsoleScreenBufferInfo() # Populated by GetConsoleScreenBufferInfo. + lpcsbi = ctypes.byref(csbi) + dword = ctypes.c_ulong() # Populated by GetConsoleMode. + lpdword = ctypes.byref(dword) + if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi) or not kernel32.GetConsoleMode(handle, lpdword): + raise ctypes.WinError() + + # Parse data. + # buffer_width = int(csbi.dwSize.X - 1) + # buffer_height = int(csbi.dwSize.Y) + # terminal_width = int(csbi.srWindow.Right - csbi.srWindow.Left) + # terminal_height = int(csbi.srWindow.Bottom - csbi.srWindow.Top) + fg_color = csbi.wAttributes % 16 + bg_color = csbi.wAttributes & 240 + native_ansi = bool(dword.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + return fg_color, bg_color, native_ansi + + +def bg_color_native_ansi(kernel32, stderr, stdout): + """Get background color and if console supports ANSI colors natively for both streams. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int stderr: stderr handle. + :param int stdout: stdout handle. + + :return: Background color (int) and native ANSI support (bool). + :rtype: tuple + """ + try: + if stderr == INVALID_HANDLE_VALUE: + raise OSError + bg_color, native_ansi = get_console_info(kernel32, stderr)[1:] + except OSError: + try: + if stdout == INVALID_HANDLE_VALUE: + raise OSError + bg_color, native_ansi = get_console_info(kernel32, stdout)[1:] + except OSError: + bg_color, native_ansi = WINDOWS_CODES['black'], False + return bg_color, native_ansi + + +class WindowsStream(object): + """Replacement stream which overrides sys.stdout or sys.stderr. When writing or printing, ANSI codes are converted. + + ANSI (Linux/Unix) color codes are converted into win32 system calls, changing the next character's color before + printing it. Resources referenced: + https://github.com/tartley/colorama + http://www.cplusplus.com/articles/2ywTURfi/ + http://thomasfischer.biz/python-and-windows-terminal-colors/ + http://stackoverflow.com/questions/17125440/c-win32-console-color + http://www.tysos.org/svn/trunk/mono/corlib/System/WindowsConsoleDriver.cs + http://stackoverflow.com/questions/287871/print-in-terminal-with-colors-using-python + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682088#_win32_character_attributes + + :cvar list ALL_BG_CODES: List of bg Windows codes. Used to determine if requested color is foreground or background. + :cvar dict COMPILED_CODES: Translation dict. Keys are ANSI codes (values of BASE_CODES), values are Windows codes. + :ivar int default_fg: Foreground Windows color code at the time of instantiation. + :ivar int default_bg: Background Windows color code at the time of instantiation. + """ + + ALL_BG_CODES = [v for k, v in WINDOWS_CODES.items() if k.startswith('bg') or k.startswith('hibg')] + COMPILED_CODES = dict((v, WINDOWS_CODES[k]) for k, v in BASE_CODES.items() if k in WINDOWS_CODES) + + def __init__(self, kernel32, stream_handle, original_stream): + """Constructor. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int stream_handle: stderr or stdout handle. + :param original_stream: sys.stderr or sys.stdout before being overridden by this class' instance. + """ + self._kernel32 = kernel32 + self._stream_handle = stream_handle + self._original_stream = original_stream + self.default_fg, self.default_bg = self.colors + + def __getattr__(self, item): + """If an attribute/function/etc is not defined in this function, retrieve the one from the original stream. + + Fixes ipython arrow key presses. + """ + return getattr(self._original_stream, item) + + @property + def colors(self): + """Return the current foreground and background colors.""" + try: + return get_console_info(self._kernel32, self._stream_handle)[:2] + except OSError: + return WINDOWS_CODES['white'], WINDOWS_CODES['black'] + + @colors.setter + def colors(self, color_code): + """Change the foreground and background colors for subsequently printed characters. + + None resets colors to their original values (when class was instantiated). + + Since setting a color requires including both foreground and background codes (merged), setting just the + foreground color resets the background color to black, and vice versa. + + This function first gets the current background and foreground colors, merges in the requested color code, and + sets the result. + + However if we need to remove just the foreground color but leave the background color the same (or vice versa) + such as when {/red} is used, we must merge the default foreground color with the current background color. This + is the reason for those negative values. + + :param int color_code: Color code from WINDOWS_CODES. + """ + if color_code is None: + color_code = WINDOWS_CODES['/all'] + + # Get current color code. + current_fg, current_bg = self.colors + + # Handle special negative codes. Also determine the final color code. + if color_code == WINDOWS_CODES['/fg']: + final_color_code = self.default_fg | current_bg # Reset the foreground only. + elif color_code == WINDOWS_CODES['/bg']: + final_color_code = current_fg | self.default_bg # Reset the background only. + elif color_code == WINDOWS_CODES['/all']: + final_color_code = self.default_fg | self.default_bg # Reset both. + elif color_code == WINDOWS_CODES['bgblack']: + final_color_code = current_fg # Black background. + else: + new_is_bg = color_code in self.ALL_BG_CODES + final_color_code = color_code | (current_fg if new_is_bg else current_bg) + + # Set new code. + self._kernel32.SetConsoleTextAttribute(self._stream_handle, final_color_code) + + def write(self, p_str): + """Write to stream. + + :param str p_str: string to print. + """ + for segment in RE_SPLIT.split(p_str): + if not segment: + # Empty string. p_str probably starts with colors so the first item is always ''. + continue + if not RE_SPLIT.match(segment): + # No color codes, print regular text. + print(segment, file=self._original_stream, end='') + self._original_stream.flush() + continue + for color_code in (int(c) for c in RE_NUMBER_SEARCH.findall(segment)[0].split(';')): + if color_code in self.COMPILED_CODES: + self.colors = self.COMPILED_CODES[color_code] + + +class Windows(object): + """Enable and disable Windows support for ANSI color character codes. + + Call static method Windows.enable() to enable color support for the remainder of the process' lifetime. + + This class is also a context manager. You can do this: + with Windows(): + print(Color('{autored}Test{/autored}')) + + Or this: + with Windows(auto_colors=True): + print(Color('{autored}Test{/autored}')) + """ + + @classmethod + def disable(cls): + """Restore sys.stderr and sys.stdout to their original objects. Resets colors to their original values. + + :return: If streams restored successfully. + :rtype: bool + """ + # Skip if not on Windows. + if not IS_WINDOWS: + return False + + # Restore default colors. + if hasattr(sys.stderr, '_original_stream'): + getattr(sys, 'stderr').color = None + if hasattr(sys.stdout, '_original_stream'): + getattr(sys, 'stdout').color = None + + # Restore original streams. + changed = False + if hasattr(sys.stderr, '_original_stream'): + changed = True + sys.stderr = getattr(sys.stderr, '_original_stream') + if hasattr(sys.stdout, '_original_stream'): + changed = True + sys.stdout = getattr(sys.stdout, '_original_stream') + + return changed + + @staticmethod + def is_enabled(): + """Return True if either stderr or stdout has colors enabled.""" + return hasattr(sys.stderr, '_original_stream') or hasattr(sys.stdout, '_original_stream') + + @classmethod + def enable(cls, auto_colors=False, reset_atexit=False): + """Enable color text with print() or sys.stdout.write() (stderr too). + + :param bool auto_colors: Automatically selects dark or light colors based on current terminal's background + color. Only works with {autored} and related tags. + :param bool reset_atexit: Resets original colors upon Python exit (in case you forget to reset it yourself with + a closing tag). Does nothing on native ANSI consoles. + + :return: If streams replaced successfully. + :rtype: bool + """ + if not IS_WINDOWS: + return False # Windows only. + + # Get values from init_kernel32(). + kernel32, stderr, stdout = init_kernel32() + if stderr == INVALID_HANDLE_VALUE and stdout == INVALID_HANDLE_VALUE: + return False # No valid handles, nothing to do. + + # Get console info. + bg_color, native_ansi = bg_color_native_ansi(kernel32, stderr, stdout) + + # Set auto colors: + if auto_colors: + if bg_color in (112, 96, 240, 176, 224, 208, 160): + ANSICodeMapping.set_light_background() + else: + ANSICodeMapping.set_dark_background() + + # Don't replace streams if ANSI codes are natively supported. + if native_ansi: + return False + + # Reset on exit if requested. + if reset_atexit: + atexit.register(cls.disable) + + # Overwrite stream references. + if stderr != INVALID_HANDLE_VALUE: + sys.stderr.flush() + sys.stderr = WindowsStream(kernel32, stderr, sys.stderr) + if stdout != INVALID_HANDLE_VALUE: + sys.stdout.flush() + sys.stdout = WindowsStream(kernel32, stdout, sys.stdout) + + return True + + def __init__(self, auto_colors=False): + """Constructor.""" + self.auto_colors = auto_colors + + def __enter__(self): + """Context manager, enables colors on Windows.""" + self.enable(auto_colors=self.auto_colors) + + def __exit__(self, *_): + """Context manager, disabled colors on Windows.""" + self.disable() -- cgit v1.2.3