2

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

netzego
  • 372
  • 4
  • 15
fferri
  • 18,285
  • 5
  • 46
  • 95
  • remove `b` from `'rb'` in `os.fdopen(sys.stdin.fileno(), 'rb')`? – Moon Cheesez Jun 09 '16 at 12:34
  • @MoonCheesez did you read that I'm using python 3? :P – fferri Jun 09 '16 at 18:42
  • Just hazarding a guess. `'b'` usually means buffer so removing it might help. – Moon Cheesez Jun 10 '16 at 00:05
  • 3
    @MoonCheesez this does not work by guessing. It takes 5 seconds to verify that what you are suggesting is wrong as it produces an error, so why do you keep wasting time with random guesses? – fferri Jun 10 '16 at 08:54
  • 4
    @MoonCheesez also, "`b` usually means buffer" is wrong. Please refrain from commenting/answering if you are not *sure* of your claims. – fferri Jun 10 '16 at 09:01

2 Answers2

5

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]))
netzego
  • 372
  • 4
  • 15
1

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

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • the problem of using curses is that it requires you to run `curses.initscr()`, which causes the unwanted side effect of the terminal being cleared – fferri Jun 09 '16 at 18:40