1

I try to implement getch() function in Python, which should also return list of chars for special keys like F1-F12 and arrow keys. These special keys generate several chars in a sequence. Therefore getch() reads one char in blocking mode and then should check if there are extra chars in input buffer to fetch them too.

I am using ioctl call together with termios.FIONREAD to get the number of bytes in the input buffer. It catches non-special key presses stacked in buffer, but misses extra symbols from special keys. It seems like there are two different buffers and it would be nice if somebody could explain this.

Here is the interactive example:

from time import sleep

def getch():
    import sys, tty, termios
    fd = sys.stdin.fileno()
    # save old terminal settings, because we are changing them
    old_settings = termios.tcgetattr(fd)
    try:
        # set terminal to "raw" mode, in which driver returns
        # one char at a time instead of one line at a time
        #
        # tty.setraw() is just a helper for tcsetattr() call, see
        # http://hg.python.org/cpython/file/c6880edaf6f3/Lib/tty.py
        tty.setraw(fd)
        ch = sys.stdin.read(1)

        # --- check if there are more characters in buffer
        from fcntl import ioctl
        from array import array

        sleep(1)
        buf = array('i', [0])
        ioctl(fd, termios.FIONREAD, buf)
        print "buf queue: %s," % buf[0],
        # ---

    finally:
        # restore terminal settings. Do this when all output is
        # finished - TCSADRAIN flag
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

char = ''
while char != 'q':
  char = getch()
  print 'sym: %s, ord(%s)' % (char, ord(char))

Note sleep(1) in the middle. If you hit one key before this second expires, the output will be:

buf queue: 0, sym: l, ord(108)

For 5 ordinary keys (for example 'asdfg') entered in one second, the output is:

buf queue: 4, sym: a, ord(97)

but for a single arrow key, the output:

buf queue: 0, sym: , ord(27)
buf queue: 0, sym: [, ord(91)
buf queue: 0, sym: D, ord(68)

There are two questions here:

  1. Why 4 symbols in queue with ordinary key presses are discarded? Is it because of switch to "raw" terminal mode? How is it possible to preserve chars for subsequent getch() runs without leaving terminal in "raw" mode?

  2. Why the ioctl buffer for a single special key press is empty? Where are those characters are coming from for subsequent getch() runs? How to check for them?

anatoly techtonik
  • 19,847
  • 9
  • 124
  • 140
  • possible duplicate of [Reading a single character (getch style) in Python is not working in Unix](http://stackoverflow.com/questions/1052107/reading-a-single-character-getch-style-in-python-is-not-working-in-unix) – S.Lott Dec 23 '11 at 22:21
  • Not even close for a duplicate. – anatoly techtonik Dec 23 '11 at 23:40

1 Answers1

2

I ran into this same issue. Some searching yielded a working example that read at most 4 bytes (instead of your 1) to allow for special escape sequences and used os.read (instead of your file.read). Based on those differences, I was able to write a little Keyboard class that recognized cursor key events:

#!/usr/bin/env python

import os
import select
import sys
import termios

class Keyboard:
  ESCAPE = 27
  LEFT = 1000
  RIGHT = 1001
  DOWN = 1002
  UP = 1003

  keylist = {
    '\x1b' : ESCAPE,
    '\x1b[A' : UP,
    '\x1b[B' : DOWN,
    '\x1b[C' : RIGHT,
    '\x1b[D' : LEFT,
  }

  def __init__(self):
    self.fd = sys.stdin.fileno()
    self.old = termios.tcgetattr(self.fd)
    self.new = termios.tcgetattr(self.fd)
    self.new[3] = self.new[3] & ~termios.ICANON & ~termios.ECHO
    self.new[6][termios.VMIN] = 1
    self.new[6][termios.VTIME] = 0
    termios.tcsetattr(self.fd, termios.TCSANOW, self.new)

  def __enter__(self):
    return self

  def __exit__(self, type, value, traceback):
    termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)

  def getFile(self):
    return self.fd

  def read(self):
    keys = os.read(self.fd, 4)
    if keys in Keyboard.keylist:
      return Keyboard.keylist[keys]
    else:
      return None

if __name__ == "__main__":
  with Keyboard() as keyboard:
    key = keyboard.read()
    while key != Keyboard.ESCAPE:
      print '%d' % key
      key = keyboard.read()

With file.read(4), the reading blocks. With os.read(fd, 4), the reading does not block. I don't know why there's a difference and would welcome enlightenment.

kaerimasu
  • 690
  • 3
  • 17
  • Good catch. I guess file.read() does some buffering on its own. – anatoly techtonik Dec 18 '12 at 12:28
  • It still doesn't work as expected. If you insert sleep(2) in the last while and press ESC 4 times during the pause, you'll get string with 4 ESC codes on the next os.read() operation. Expected is to get string with one ESC for every read. – anatoly techtonik Jul 24 '13 at 14:24
  • I gave up - it looks like Linux just doesn't allow this stuff. I asked here to be sure - http://stackoverflow.com/questions/17838339/is-there-a-typeahead-buffer-for-key-presses-in-linux-terminal – anatoly techtonik Jul 24 '13 at 15:25