How to detect if the console does support ANSI escape codes in Python?
In order to de开发者_高级运维tect if console, correctly sys.stderr
or sys.stdout
, I was doing the following test:
if hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
if platform.system()=='Windows':
# win code (ANSI not supported but there are alternatives)
else:
# use ANSI escapes
else:
# no colors, usually this is when you redirect the output to a file
Now the problem became more complex while running this Python code via an IDE (like PyCharm). Recently PyCharm added support for ANSI, but the first test fails: it has the isatty
attribute but it is set to False
.
I want to modify the logic so it will properly detect if the output supports ANSI coloring. One requirement is that under no circumstance I should output something out when the output is redirected to a file (for console it would be acceptable).
Update
Added more complex ANSI test script at https://gist.github.com/1316877
Django users can use django.core.management.color.supports_color
function.
if supports_color():
...
The code they use is:
def supports_color():
"""
Returns True if the running system's terminal supports color, and False
otherwise.
"""
plat = sys.platform
supported_platform = plat != 'Pocket PC' and (plat != 'win32' or
'ANSICON' in os.environ)
# isatty is not always implemented, #6223.
is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
return supported_platform and is_a_tty
See https://github.com/django/django/blob/master/django/core/management/color.py
I can tell you how others have solved this problem, but it's not pretty. If you look at ncurses as an example (which needs to be able to run on all kinds of different terminals), you'll see that they use a terminal capabilities database to store every kind of terminal and its capabilities. The point being, even they were never able to automatically "detect" these things.
I don't know if there's a cross-platform termcap, but it's probably worth your time to look for it. Even if it's out there though, it may not have your terminal listed and you may have to manually add it.
\x1B[6n
is a standard (as far as I am aware) ANSI escape code to query the position of the user's cursor. If sent to stdout, the terminal should write \x1B[{line};{column}R
to stdin. It can be assumed that ANSI escape codes are supported if this result is achieved. The main problem becomes detecting this reply.
Windows
msvcrt.getch
can be used to retrieve a char from stdin, without waiting for enter to be pressed. This in combination with msvcrt.kbhit
, which detects if stdin is waiting to be read yields code found in the Code w/ Comments section of this post.
Unix/with termios
Warning: I have (inadvisably) not tested this specific tty/select/termios code, but have known similiar code to work in the past.
getch
and kbhit
can be replicated using tty.setraw
and select.select
. Thus we can define these functions as follows:
from termios import TCSADRAIN, tcgetattr, tcsetattr
from select import select
from tty import setraw
from sys import stdin
def getch() -> bytes:
fd = stdin.fileno() # get file descriptor of stdin
old_settings = tcgetattr(fd) # save settings (important!)
try: # setraw accomplishes a few things,
setraw(fd) # such as disabling echo and wait.
return stdin.read(1).encode() # consistency with the Windows func
finally: # means output should be in bytes
tcsetattr(fd, TCSADRAIN, old_settings) # finally, undo setraw (important!)
def kbhit() -> bool: # select.select checks if fds are
return bool(select([stdin], [], [], 0)[0]) # ready for reading/writing/error
This can then be used with the below code.
Code w/ Comments
from sys import stdin, stdout
def isansitty() -> bool:
"""
The response to \x1B[6n should be \x1B[{line};{column}R according to
https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797. If this
doesn't work, then it is unlikely ANSI escape codes are supported.
"""
while kbhit(): # clear stdin before sending escape in
getch() # case user accidentally presses a key
stdout.write("\x1B[6n") # alt: print(end="\x1b[6n", flush=True)
stdout.flush() # double-buffered stdout needs flush
stdin.flush() # flush stdin to make sure escape works
if kbhit(): # ANSI won't work if stdin is empty
if ord(getch()) == 27 and kbhit(): # check that response starts with \x1B[
if getch() == b"[":
while kbhit(): # read stdin again, to remove the actual
getch() # value returned by the escape
return stdout.isatty() # lastly, if stdout is a tty, ANSI works
# so True should be returned. Otherwise,
return False # return False
Complete Code w/o Comments
In case you want it, here is the raw code.
from sys import stdin, stdout
from platform import system
if system() == "Windows":
from msvcrt import getch, kbhit
else:
from termios import TCSADRAIN, tcgetattr, tcsetattr
from select import select
from tty import setraw
from sys import stdin
def getch() -> bytes:
fd = stdin.fileno()
old_settings = tcgetattr(fd)
try:
setraw(fd)
return stdin.read(1).encode()
finally:
tcsetattr(fd, TCSADRAIN, old_settings)
def kbhit() -> bool:
return bool(select([stdin], [], [], 0)[0])
def isansitty() -> bool:
"""
Checks if stdout supports ANSI escape codes and is a tty.
"""
while kbhit():
getch()
stdout.write("\x1b[6n")
stdout.flush()
stdin.flush()
if kbhit():
if ord(getch()) == 27 and kbhit():
if getch() == b"[":
while kbhit():
getch()
return stdout.isatty()
return False
Sources
In no particular order:
- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
- https://docs.python.org/3/library/select.html
- https://docs.python.org/3/library/tty.html
- https://docs.python.org/3/library/termios.html
- https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html#user-input
- https://stackoverflow.com/a/1052115/15081390
精彩评论