388 lines
15 KiB
Python
388 lines
15 KiB
Python
"""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()
|