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

Adds support for reverse video and underline on Windows 10. #267

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
@@ -1,3 +1,10 @@
0.4.4
* Added support for REVERSE video and UNDERLINE.
* ANSI codes CSI [ 0-37 m no longer render as BRIGHT if the default terminal
foreground color is BRIGHT.
* The demo01.py file now demonstrates reverse video and underlining effects.
* winterm_test.py updated to assert on term._default_style rather than _style.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this change is prominent enough to appear in the changelog. It's more "internal".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may have been a bit over-zealous there. Definitely agree about the last two not being in the changelog. The first two result in a change in behaviour, especially no longer getting a BRIGHT foreground colour with CSI [ 30-37 m. That should be documented somewhere, but I'm happy to remove it from the changelog.

This is in the testInit and testResetAll tests - _style has BRIGHT cleared.
0.4.3
* Fix release 0.4.2 which was uploaded with missing files.
0.4.2
Expand Down
29 changes: 28 additions & 1 deletion README.rst
Expand Up @@ -143,7 +143,27 @@ Available formatting constants are::

Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET.
Style: DIM, NORMAL, BRIGHT, RESET_ALL
Style: DIM, NORMAL, BRIGHT, BRIGHT_OFF, REVERSE, UNDERLINE, RESET_ALL

``Style.REVERSE_OFF`` and ``Style.UNDERLINE_OFF`` are provided to allow intent
Copy link
Collaborator

@wiggin15 wiggin15 Feb 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, regular ANSI-compliant terminals do have a special code for REVERSE_OFF and UNDERLINE_OFF, but Windows doesn't have an attribute to "clear" these.
Does this mean that we'll have different behaviors on Windows/Linux? e.g. the following two calls
print(colorama.Style.REVERSE_OFF + colorama.Back.GREEN + "off?" + colorama.Style.RESET_ALL)
and
print(colorama.Style.REVERSE_OFF + colorama.Style.REVERSE_OFF + colorama.Back.GREEN + "off?" + colorama.Style.RESET_ALL)
will have "REVERSE" off in both on Linux, but will have "REVERSE" on in the first call on Windows?
Maybe we can call a different function, or use a special value in WinStyle to make sure OFF means OFF?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the behaviour I expected (and intended) but there seems to be a problem there. I'll work on that (after rebasing to the current version).
I think the Windows behaviour can be fixed by tracking the current state of REVERSE and UNDERLINE. I'll make sure OFF really means off (and ON doesn't mean OFF if the attribute is already set).

to be clearer.
They currently have the same values as ``REVERSE`` and ``UNDERLINE``,
respectively. Using ``REVERSE`` a 2nd time will toggle REVERSE off again
(and the same for UNDERLINE).

It's important to realise that ``Style.UNDERLINE_OFF`` and ``Style.REVERSE_OFF``
are not guaranteed to turn the relevant effect off. If you've called
``colorama.init`` with ``autoreset=False``, and left the terminal in underline
mode, then the next time either ``Style.UNDERLINE`` or ``Style.UNDERLINE_OFF``
is encountered, underlining will be turned off.

Note that REVERSE and UNDERLINE require Windows 10, they don't have any effect
on Windows 7 or 8.
jpwroberts marked this conversation as resolved.
Show resolved Hide resolved

``BRIGHT_OFF`` is included because it's a valid ANSI sequence. It will do the
same thing as ``NORMAL``, on Windows.

On some terminals, it will produce a double underline.

``Style.RESET_ALL`` resets foreground, background, and brightness. Colorama will
perform this reset automatically on program exit.
Expand Down Expand Up @@ -254,7 +274,14 @@ The only ANSI sequences that colorama converts into win32 calls are::
ESC [ 0 m # reset all (colors and brightness)
ESC [ 1 m # bright
ESC [ 2 m # dim (looks same as normal brightness)
ESC [ 4 m # underline *1*
ESC [ 7 m # reverse video *1*
ESC [ 21 m # double underline or normal brightness
ESC [ 22 m # normal brightness
ESC [ 24 m # underline off *1*
ESC [ 27 m # reverse video off *1*

*1. Not supported on Windows 7 or 8.*
jpwroberts marked this conversation as resolved.
Show resolved Hide resolved

# FOREGROUND:
ESC [ 30 m # black
Expand Down
27 changes: 21 additions & 6 deletions colorama/ansi.py
@@ -1,8 +1,8 @@
# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file.
'''
"""
This module generates ANSI character codes to printing colors to terminals.
See: http://en.wikipedia.org/wiki/ANSI_escape_code
'''
"""

CSI = '\033['
OSC = '\033]'
Expand All @@ -12,12 +12,15 @@
def code_to_chars(code):
return CSI + str(code) + 'm'


def set_title(title):
return OSC + '2;' + title + BEL


def clear_screen(mode=2):
return CSI + str(mode) + 'J'


def clear_line(mode=2):
return CSI + str(mode) + 'K'

Expand All @@ -34,14 +37,19 @@ def __init__(self):


class AnsiCursor(object):

def UP(self, n=1):
return CSI + str(n) + 'A'

def DOWN(self, n=1):
return CSI + str(n) + 'B'

def FORWARD(self, n=1):
return CSI + str(n) + 'C'

def BACK(self, n=1):
return CSI + str(n) + 'D'

def POS(self, x=1, y=1):
return CSI + str(y) + ';' + str(x) + 'H'

Expand Down Expand Up @@ -91,10 +99,17 @@ class AnsiBack(AnsiCodes):


class AnsiStyle(AnsiCodes):
BRIGHT = 1
DIM = 2
NORMAL = 22
RESET_ALL = 0
BRIGHT = 1
DIM = 2
BRIGHT_OFF = 21 # Produces double-underline on some terminals.
NORMAL = 22
RESET_ALL = 0

UNDERLINE = 4
UNDERLINE_OFF = 24
REVERSE = 7
REVERSE_OFF = 27


Fore = AnsiFore()
Back = AnsiBack()
Expand Down
30 changes: 15 additions & 15 deletions colorama/ansitowin32.py
Expand Up @@ -14,11 +14,11 @@


class StreamWrapper(object):
'''
"""
Wraps a stream (such as stdout), acting as a transparent proxy for all
attribute access apart from method 'write()', which is delegated to our
Converter instance.
'''
"""
def __init__(self, wrapped, converter):
# double-underscore everything to prevent clashes with names of
# attributes on the wrapped stream object.
Expand Down Expand Up @@ -62,11 +62,11 @@ def closed(self):


class AnsiToWin32(object):
'''
"""
Implements a 'write()' method which, on Windows, will strip ANSI character
sequences from the text, and if outputting to a tty, will convert them into
win32 function calls.
'''
"""
ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer
ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command

Expand Down Expand Up @@ -104,13 +104,13 @@ def __init__(self, wrapped, convert=None, strip=None, autoreset=False):
self.on_stderr = self.wrapped is sys.stderr

def should_wrap(self):
'''
"""
True if this class is actually needed. If false, then the output
stream will not be affected, nor will win32 calls be issued, so
wrapping stdout is not actually required. This will generally be
False on non-Windows platforms, unless optional functionality like
autoreset has been requested using kwargs to init()
'''
"""
return self.convert or self.strip or self.autoreset

def get_win32_calls(self):
Expand All @@ -120,6 +120,13 @@ def get_win32_calls(self):
AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT),
AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL),
AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL),
AnsiStyle.BRIGHT_OFF: (winterm.style, WinStyle.NORMAL),

AnsiStyle.UNDERLINE: (winterm.style, WinStyle.UNDERLINE),
AnsiStyle.UNDERLINE_OFF: (winterm.style, WinStyle.UNDERLINE_OFF),
AnsiStyle.REVERSE: (winterm.style, WinStyle.REVERSE),
AnsiStyle.REVERSE_OFF: (winterm.style, WinStyle.REVERSE_OFF),

AnsiFore.BLACK: (winterm.fore, WinColor.BLACK),
AnsiFore.RED: (winterm.fore, WinColor.RED),
AnsiFore.GREEN: (winterm.fore, WinColor.GREEN),
Expand Down Expand Up @@ -166,20 +173,18 @@ def write(self, text):
if self.autoreset:
self.reset_all()


def reset_all(self):
if self.convert:
self.call_win32('m', (0,))
elif not self.strip and not self.stream.closed:
self.wrapped.write(Style.RESET_ALL)


def write_and_convert(self, text):
'''
"""
Write the given text to our wrapped stream, stripping any ANSI
sequences from the text, and optionally converting them into win32
calls.
'''
"""
cursor = 0
text = self.convert_osc(text)
for match in self.ANSI_CSI_RE.finditer(text):
Expand All @@ -189,19 +194,16 @@ def write_and_convert(self, text):
cursor = end
self.write_plain_text(text, cursor, len(text))


def write_plain_text(self, text, start, end):
if start < end:
self.wrapped.write(text[start:end])
self.wrapped.flush()


def convert_ansi(self, paramstring, command):
if self.convert:
params = self.extract_params(command, paramstring)
self.call_win32(command, params)


def extract_params(self, command, paramstring):
if command in 'Hf':
params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';'))
Expand All @@ -219,7 +221,6 @@ def extract_params(self, command, paramstring):

return params


def call_win32(self, command, params):
if command == 'm':
for param in params:
Expand All @@ -241,7 +242,6 @@ def call_win32(self, command, params):
x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command]
winterm.cursor_adjust(x, y, on_stderr=self.on_stderr)


def convert_osc(self, text):
for match in self.ANSI_OSC_RE.finditer(text):
start, end = match.span()
Expand Down
6 changes: 4 additions & 2 deletions colorama/tests/winterm_test.py
Expand Up @@ -20,7 +20,8 @@ def testInit(self, mockWin32):
term = WinTerm()
self.assertEqual(term._fore, 7)
self.assertEqual(term._back, 6)
self.assertEqual(term._style, 8)
# _default_style should be correct. _style will have been reset to 0
self.assertEqual(term._default_style, 8)

@skipUnless(sys.platform.startswith("win"), "requires Windows")
def testGetAttrs(self):
Expand Down Expand Up @@ -60,7 +61,8 @@ def testResetAll(self, mockWin32):

self.assertEqual(term._fore, 1)
self.assertEqual(term._back, 2)
self.assertEqual(term._style, 8)
# _default_style should be correct. _style will have been reset to 0
self.assertEqual(term._default_style, 8)
self.assertEqual(term.set_console.called, True)

@skipUnless(sys.platform.startswith("win"), "requires Windows")
Expand Down
45 changes: 41 additions & 4 deletions colorama/winterm.py
Expand Up @@ -13,11 +13,18 @@ class WinColor(object):
YELLOW = 6
GREY = 7


# from wincon.h
class WinStyle(object):
NORMAL = 0x00 # dim text, dim background
BRIGHT = 0x08 # bright text, dim background
BRIGHT_BACKGROUND = 0x80 # dim text, bright background
NORMAL = 0x00 # dim text, dim background
BRIGHT = 0x08 # bright text, dim background
BRIGHT_BACKGROUND = 0x80 # dim text, bright background

REVERSE = 0x4000
REVERSE_OFF = REVERSE
UNDERLINE = 0x8000
UNDERLINE_OFF = UNDERLINE


class WinTerm(object):

Expand All @@ -27,6 +34,12 @@ def __init__(self):
self._default_fore = self._fore
self._default_back = self._back
self._default_style = self._style

# Cater for the default console using bright foreground.
# If we don't clear BRIGHT, normal text will render as bold.
if self._default_style & WinStyle.BRIGHT:
self._style &= ~WinStyle.BRIGHT

# In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style.
# So that LIGHT_EX colors and BRIGHT style do not clobber each other,
# we track them separately, since LIGHT_EX is overwritten by Fore/Back
Expand All @@ -45,11 +58,16 @@ def reset_all(self, on_stderr=None):
self.set_attrs(self._default)
self.set_console(attrs=self._default)
self._light = 0
self._style = self._default_style
# Turn off BRIGHT if that's the default for the console foreground.
if self._default_style & WinStyle.BRIGHT:
self._style &= ~WinStyle.BRIGHT

def fore(self, fore=None, light=False, on_stderr=False):
if fore is None:
fore = self._default_fore
self._fore = fore

# Emulate LIGHT_EX with BRIGHT Style
if light:
self._light |= WinStyle.BRIGHT
Expand All @@ -71,7 +89,26 @@ def back(self, back=None, light=False, on_stderr=False):
def style(self, style=None, on_stderr=False):
if style is None:
style = self._default_style
self._style = style
reverse_set = self._style & WinStyle.REVERSE
underline_set = self._style & WinStyle.UNDERLINE

# Preserve any existing style(s) - this allows things
# like bold *and* underline.
# Note that NORMAL (0) would reset everything. We only want
# it to reset BRIGHT or DIM.
if style:
self._style |= style
else:
self._style = style | reverse_set | underline_set

# Now deal with turning attributes off. SetConsoleTextAttribute
# doesn't currently have attributes to cancel bold, reverse and underline
# https://docs.microsoft.com/en-us/windows/console/console-screen-buffers
if reverse_set:
self._style &= ~(style & WinStyle.REVERSE_OFF)
if underline_set:
self._style &= ~(style & WinStyle.UNDERLINE_OFF)

self.set_console(on_stderr=on_stderr)

def set_console(self, attrs=None, on_stderr=False):
Expand Down
4 changes: 2 additions & 2 deletions demos/demo01.py
Expand Up @@ -18,7 +18,7 @@
# the foreground, background and style. The don't have any magic of their own.
FORES = [ Fore.BLACK, Fore.RED, Fore.GREEN, Fore.YELLOW, Fore.BLUE, Fore.MAGENTA, Fore.CYAN, Fore.WHITE ]
BACKS = [ Back.BLACK, Back.RED, Back.GREEN, Back.YELLOW, Back.BLUE, Back.MAGENTA, Back.CYAN, Back.WHITE ]
STYLES = [ Style.DIM, Style.NORMAL, Style.BRIGHT ]
STYLES = [ Style.DIM, Style.NORMAL, Style.BRIGHT, Style.UNDERLINE, Style.REVERSE ]

NAMES = {
Fore.BLACK: 'black', Fore.RED: 'red', Fore.GREEN: 'green', Fore.YELLOW: 'yellow', Fore.BLUE: 'blue', Fore.MAGENTA: 'magenta', Fore.CYAN: 'cyan', Fore.WHITE: 'white'
Expand All @@ -30,7 +30,7 @@
# show the color names
sys.stdout.write(' ')
for foreground in FORES:
sys.stdout.write('%s%-7s' % (foreground, NAMES[foreground]))
sys.stdout.write('%s%-11s' % (foreground, NAMES[foreground]))
print()

# make a row for each background color
Expand Down
Binary file modified screenshots/ubuntu-demo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified screenshots/windows-demo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.