Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yet another attempt at supporting Windows 10's ANSI/VT console #139

Merged
merged 8 commits into from Oct 16, 2022
13 changes: 10 additions & 3 deletions colorama/ansitowin32.py
Expand Up @@ -4,7 +4,7 @@
import os

from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL
from .winterm import WinTerm, WinColor, WinStyle
from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle
from .win32 import windll, winapi_test


Expand Down Expand Up @@ -94,15 +94,22 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
# (e.g. Cygwin Terminal). In this case it's up to the terminal
# to support the ANSI codes.
conversion_supported = on_windows and winapi_test()
try:
fd = wrapped.fileno()
except Exception:
fd = -1
system_has_native_ansi = not on_windows or enable_vt_processing(fd)
have_tty = not self.stream.closed and self.stream.isatty()
need_conversion = conversion_supported and not system_has_native_ansi

# should we strip ANSI sequences from our output?
if strip is None:
strip = conversion_supported or (not self.stream.closed and not self.stream.isatty())
strip = need_conversion or not have_tty
self.strip = strip

# should we should convert ANSI sequences into win32 calls?
if convert is None:
convert = conversion_supported and not self.stream.closed and self.stream.isatty()
convert = need_conversion and have_tty
self.convert = convert

# dict of ansi codes to win32 functions and parameters
Expand Down
51 changes: 51 additions & 0 deletions colorama/tests/ansitowin32_test.py
@@ -1,13 +1,19 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from io import StringIO, TextIOWrapper
from unittest import TestCase, main
try:
from contextlib import ExitStack
except ImportError:
# python 2
from contextlib2 import ExitStack

try:
from unittest.mock import MagicMock, Mock, patch
except ImportError:
from mock import MagicMock, Mock, patch

from ..ansitowin32 import AnsiToWin32, StreamWrapper
from ..win32 import ENABLE_VIRTUAL_TERMINAL_PROCESSING
from .utils import osname


Expand Down Expand Up @@ -239,5 +245,50 @@ def test_osc_codes(self):
stream.write(code)
self.assertEqual(winterm.set_title.call_count, 2)

def test_native_windows_ansi(self):
with ExitStack() as stack:
def p(a, b):
stack.enter_context(patch(a, b, create=True))
# Pretend to be on Windows
p("colorama.ansitowin32.os.name", "nt")
p("colorama.ansitowin32.winapi_test", lambda: True)
p("colorama.win32.winapi_test", lambda: True)
p("colorama.winterm.win32.windll", "non-None")
p("colorama.winterm.get_osfhandle", lambda _: 1234)

# Pretend that our mock stream has native ANSI support
p(
"colorama.winterm.win32.GetConsoleMode",
lambda _: ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)
SetConsoleMode = Mock()
p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode)

stdout = Mock()
stdout.closed = False
stdout.isatty.return_value = True
stdout.fileno.return_value = 1

# Our fake console says it has native vt support, so AnsiToWin32 should
# enable that support and do nothing else.
stream = AnsiToWin32(stdout)
SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING)
self.assertFalse(stream.strip)
self.assertFalse(stream.convert)
self.assertFalse(stream.should_wrap())

# Now let's pretend we're on an old Windows console, that doesn't have
# native ANSI support.
p("colorama.winterm.win32.GetConsoleMode", lambda _: 0)
SetConsoleMode = Mock()
p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode)

stream = AnsiToWin32(stdout)
SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING)
self.assertTrue(stream.strip)
self.assertTrue(stream.convert)
self.assertTrue(stream.should_wrap())


if __name__ == '__main__':
main()
28 changes: 28 additions & 0 deletions colorama/win32.py
Expand Up @@ -4,6 +4,8 @@
STDOUT = -11
STDERR = -12

ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004

try:
import ctypes
from ctypes import LibraryLoader
Expand Down Expand Up @@ -89,6 +91,20 @@ def __str__(self):
]
_SetConsoleTitleW.restype = wintypes.BOOL

_GetConsoleMode = windll.kernel32.GetConsoleMode
_GetConsoleMode.argtypes = [
wintypes.HANDLE,
POINTER(wintypes.DWORD)
]
_GetConsoleMode.restype = wintypes.BOOL

_SetConsoleMode = windll.kernel32.SetConsoleMode
_SetConsoleMode.argtypes = [
wintypes.HANDLE,
wintypes.DWORD
]
_SetConsoleMode.restype = wintypes.BOOL

def _winapi_test(handle):
csbi = CONSOLE_SCREEN_BUFFER_INFO()
success = _GetConsoleScreenBufferInfo(
Expand Down Expand Up @@ -150,3 +166,15 @@ def FillConsoleOutputAttribute(stream_id, attr, length, start):

def SetConsoleTitle(title):
return _SetConsoleTitleW(title)

def GetConsoleMode(handle):
mode = wintypes.DWORD()
success = _GetConsoleMode(handle, byref(mode))
if not success:
raise ctypes.WinError()
return mode.value

def SetConsoleMode(handle, mode):
success = _SetConsoleMode(handle, mode)
if not success:
raise ctypes.WinError()
27 changes: 26 additions & 1 deletion colorama/winterm.py
@@ -1,6 +1,12 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
from . import win32
try:
from msvcrt import get_osfhandle
except ImportError:
def get_osfhandle(_):
raise OSError("This isn't windows!")


from . import win32

# from wincon.h
class WinColor(object):
Expand Down Expand Up @@ -167,3 +173,22 @@ def erase_line(self, mode=0, on_stderr=False):

def set_title(self, title):
win32.SetConsoleTitle(title)


def enable_vt_processing(fd):
if win32.windll is None or not win32.winapi_test():
return False

try:
handle = get_osfhandle(fd)
mode = win32.GetConsoleMode(handle)
win32.SetConsoleMode(
handle,
mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)

mode = win32.GetConsoleMode(handle)
if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING:
return True
except OSError:
return False
1 change: 1 addition & 0 deletions requirements-dev.txt
@@ -1,4 +1,5 @@
mock>=1.0.1;python_version<"3.3"
twine>=3.1.1
contextlib2;python_version<"3"
build
-e .
4 changes: 3 additions & 1 deletion tox.ini
Expand Up @@ -3,5 +3,7 @@ isolated_build = true
envlist = py27, py37, py38, py39, py310, pypy, pypy3

[testenv]
deps = py27,pypy: mock
deps =
py27,pypy: mock
py27,pypy: contextlib2
commands = python -m unittest discover -p *_test.py