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