Search code examples
pythonuser-inputstdin

Read backspace, DEL and friends from stdin in pure Python?


If one googles backspace python stdin or so, there are a lot of SO results and none that solve my problem. They are all about what the backspace char is not how to get it.


Here's a function that reads a single keypress from stdin, courtesy https://stackoverflow.com/a/6599441/4532996:

def read_single_keypress():
    import termios, fcntl, sys, os
    fd = sys.stdin.fileno()
    # save old state
    flags_save = fcntl.fcntl(fd, fcntl.F_GETFL)
    attrs_save = termios.tcgetattr(fd)
    # make raw - the way to do this comes from the termios(3) man page.
    attrs = list(attrs_save) # copy the stored version to update
    # iflag
    attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK
                  | termios.ISTRIP | termios.INLCR | termios. IGNCR
                  | termios.ICRNL | termios.IXON )
    # oflag
    attrs[1] &= ~termios.OPOST
    # cflag
    attrs[2] &= ~(termios.CSIZE | termios. PARENB)
    attrs[2] |= termios.CS8
    # lflag
    attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON
                  | termios.ISIG | termios.IEXTEN)
    termios.tcsetattr(fd, termios.TCSANOW, attrs)
    # turn off non-blocking
    fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK)
    # read a single keystroke
    try:
        ret = sys.stdin.read(1) # returns a single character
    except KeyboardInterrupt:
        ret = 0
    finally:
        # restore old state
        termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save)
        fcntl.fcntl(fd, fcntl.F_SETFL, flags_save)
    return ret

Hacky as it is, it would appear to be cross-platform.

Implementing that is a utility function from my module:

def until(char) -> str:
    """get chars of stdin until char is read"""
    import sys
    y = ""
    sys.stdout.flush()
    while True:
        i = read_single_keypress()
        _ = sys.stdout.write(i)
        sys.stdout.flush()
        if i == char or i == 0:
            break
        y += i
    return y

Which works really well, except pressing backspace does nothing, and you can't move the cursor (which import readline; input() allows you to (at least on a Python built with GNU Readline)).

I understand the fact that the best way to implement both of these is probably curses. I also understand that curses defeats the standard-libraryness and cross-platformness of this module.

What I'm looking for is a way to read stdin in a way that will capture backspace, and, for a special bonus, DEL and preferably the arrow keys too.

The module targets Pythons both 2 and 3, but I'm okay with a solution that targets just Python 3, because people really need to stop using 2.


If you think I'm flaming mad for wanting to do this without curses, well, that's the point.


Solution

  • Consider using this recipe from ActiveState that is also this SO answer:

    class _Getch:
        """Gets a single character from standard input.  Does not echo to the
    screen."""
        def __init__(self):
            try:
                self.impl = _GetchWindows()
            except ImportError:
                self.impl = _GetchUnix()
    
        def __call__(self): return self.impl()
    class _GetchUnix:
        def __init__(self):
            import tty, sys
    
        def __call__(self):
            import sys, tty, termios
            fd = sys.stdin.fileno()
            old_settings = termios.tcgetattr(fd)
            try:
                tty.setraw(sys.stdin.fileno())
                ch = sys.stdin.read(1)
            finally:
                termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
            return ch
    
    
    class _GetchWindows:
        def __init__(self):
            import msvcrt
    
        def __call__(self):
            import msvcrt
            return msvcrt.getch()
    
    
    getch = _Getch()
    # then call getch() to get the input...