How to detect if the console does support ANSI escape codes in Python?
Asked Answered
T

3

45

In order to detect 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

Towardly answered 16/9, 2011 at 13:45 Comment(0)
S
27

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

Screed answered 7/3, 2014 at 16:2 Comment(1)
This doesn't seem to work in some windows terminalsPostwar
M
13

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.

Marquez answered 16/9, 2011 at 13:55 Comment(2)
PyCharm team reported that currently there is no-way of discovering that your code is executed inside PyCharm. The workaround is to add an environment variable yourself, but is almost useless.Towardly
This PyCharm ticket discusses the issue -- youtrack.jetbrains.net/issue/PY-4853 (it references this SO discussion, and I think has some of the same participants). It documents that they have added PYCHARM_HOSTED=1 environmental variable that can be used to detect pycharm.Norine
B
7

\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:

Badtempered answered 11/3, 2023 at 9:33 Comment(2)
This works fine on Windows 10 in ConEmu64.exe (which is ANSI-codes-aware) and in cmd.exe (which is NOT ANSI-codes-aware). However, when testing in cmd.exe, extra garbage chars appears in console: ←[6nThis TTY supports ANSI codes: False. Although this does not break functionality, I'm curios if this could be elaborated (on NON-ANSI-codes-aware terminals avoid printing extra garbage symbols during testing terminal capabilities)?Aeri
I've figured out, that inserting the stdout.write('\b\b\b\b') before first print() prevents extra garbage chars in console. I'm not sure if is it a normal approach or kind of a dirty hack.Aeri

© 2022 - 2024 — McMap. All rights reserved.