Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disable buffering of sys.stdin in Python 3

I'm trying to disable stdin buffering, in order to read the response of ANSI code \033[6n (which should report the cursor position).

I tried stdin_ub = os.fdopen(stdin.fileno(), 'rb', buffering=0) as suggested in answer Setting smaller buffer size for sys.stdin?, but still the program is blocked at line ch = stdin_ub.read(1) of the first attempt to read. It unblocks when return is typed into the terminal, which suggests the stdin is still line buffered.

For reference, here's the complete code:

def getpos():
    stdin_ub = os.fdopen(sys.stdin.fileno(), 'rb', buffering=0)
    sys.stdout.write('\033[6n')
    sys.stdout.flush()
    ch, k, field = None, -1, [b'', b'']
    while True:
        #print('reading wait...')
        ch = stdin_ub.read(1)
        #print('reading OK')
        if ch == b'[': k = 0
        elif ch == b';': k = 1
        elif ch == b'R': break
        elif k >= 0: field[k] += ch
    try:
        return tuple(map(int, field))
    except:
        pass

I'm using python 3.5.1

like image 963
fferri Avatar asked Sep 15 '25 10:09

fferri


2 Answers

The trick is to use tty.setcbreak(sys.stdin.fileno(), termios.TCSANOW) and before that store the terminal attributes via termios.getattr in variable to restore the default behavior. With cbreak set, sys.stdin.read(1) is unbuffered. This also suppress the ansi controll code response from the terminal.

def getpos():

    buf = ""
    stdin = sys.stdin.fileno()
    tattr = termios.tcgetattr(stdin)

    try:
        tty.setcbreak(stdin, termios.TCSANOW)
        sys.stdout.write("\x1b[6n")
        sys.stdout.flush()

        while True:
            buf += sys.stdin.read(1)
            if buf[-1] == "R":
                break

    finally:
        termios.tcsetattr(stdin, termios.TCSANOW, tattr)

    # reading the actual values, but what if a keystroke appears while reading
    # from stdin? As dirty work around, getpos() returns if this fails: None
    try:
        matches = re.match(r"^\x1b\[(\d*);(\d*)R", buf)
        groups = matches.groups()
    except AttributeError:
        return None

    return (int(groups[0]), int(groups[1]))
like image 57
netzego Avatar answered Sep 17 '25 23:09

netzego


Unfortunately, there is no portable way to do that. The underlying IO system is line buffered when reading from keyboard on common OS, for example Windows and Unix families.

The curses module would offer an almost portable way to control the line discipline, unfortunately it does not work on windows systems.

If you can use it, you will have to use

curses.noecho()
curses.raw()   # or curses.cbreak()

to enter raw mode (generally echo should be set off)

and

curses.echo()
curses.noraw()   # resp. curses.nocbreak()

to return to normal cooked more

like image 24
Serge Ballesta Avatar answered Sep 18 '25 00:09

Serge Ballesta



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!