3

I have a loop that does some work and prints a lot of info to stdout. Over and over again (it's a loop...) What I'd like to do is to detect when / if user presses a key (it can be an arrow, enter, or a letter), and do some work when that happens.

This should have been a very simple subsubtask, but I've spent last four hours trying different approaches and getting pretty much nowhere.

This needs only work in Linux.

Best I could get is something like this below. But that works partially, catching the keys only if within the 0.05 sec.

import sys,tty,termios
class _Getch:
    def __call__(self, n=1):
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(n)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch


def getch(timeout=0.2):
    inkey = _Getch()
    k = ''
    start_sec = time()
    while(time() - start_sec < timeout):
        if k == '':
            k = timeout_call(inkey, timeout_duration=timeout - (time() - start_sec))
    if k == u'\x1b':
        k += inkey(2)
        if k == u'\x1b[A':
            return "up"
        if k == u'\x1b[B':
            return "down"
        if k == u'\x1b[C':
            return "right"
        if k == u'\x1b[D':
            return "left"
    elif k == "q":
        return 'q'
    elif k == "\n":
        return 'enter'
    else:
        return None


while True:
    do_some_work_that_lasts_about_0_2_seconds()
    key = getch(0.05)
    if key:
        do_something_with_the(key)
frnhr
  • 12,354
  • 9
  • 63
  • 90

2 Answers2

4

This has been asked before. Someone posted a nice, short, refactored solution

Reposted here

import sys
import select
import tty
import termios

class NonBlockingConsole(object):

    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)


    def get_data(self):
        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            return sys.stdin.read(1)
        return False


if __name__ == '__main__':
    # Use like this
    with NonBlockingConsole() as nbc:
        i = 0
        while 1:
            print i
            i += 1

            if nbc.get_data() == '\x1b':  # x1b is ESC
                break
Community
  • 1
  • 1
Strikeskids
  • 3,932
  • 13
  • 27
  • One problem: how can I read arrow keys and regular keys with this approach? Arrow keys seem to use three chars, but if I do `return sys.stdin.read(3)` then the console is not "non-blocking" any more. – frnhr Jul 08 '14 at 22:40
  • I'm going to accept your answer because it led me in the right direction. Thanks. But it's not really a solution for my case, because it doesn't support escape sequences. select.select seems inadequate for that. – frnhr Jul 08 '14 at 23:44
1

Here is a solution I came up with. Not perfect, because it relies on timeouts and sometimes can catch only half of escape sequences, if the key is pressed mili(micro? nano?)seconds before timeout expires. But it's the least bad solution I could come up with. Disappointing...

def timeout_call(func, args=(), kwargs=None, timeout_duration=1.0, default=None):
    if not kwargs:
        kwargs = {}
    import signal

    class TimeoutError(Exception):
        pass

    def handler(signum, frame):
        raise TimeoutError()

    # set the timeout handler
    signal.signal(signal.SIGALRM, handler)
    signal.setitimer(signal.ITIMER_REAL, timeout_duration)
    try:
        result = func(*args, **kwargs)
    except TimeoutError as exc:
        result = default
    finally:
        signal.alarm(0)

    return result


class NonBlockingConsole(object):

    def __enter__(self):
        self.old_settings = termios.tcgetattr(sys.stdin)
        tty.setcbreak(sys.stdin.fileno())
        return self

    def __exit__(self, type, value, traceback):
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)

    def get_data(self):
        k = ''
        while True:
            c = timeout_call(sys.stdin.read, args=[1], timeout_duration=0.05)
            if c is None:
                break
            k += c

        return k if k else False

Usage:

with NonBlockingConsole() as nbc:
    while True:
        sleep(0.05)  # or longer, but not shorter, for my setup anyways...
        data = nbc.get_data()
        if data:
            print data.encode('string-escape')
frnhr
  • 12,354
  • 9
  • 63
  • 90