Skip to content
Advertisement

Read a keystroke from user

I would like to read a single key from the user: letters, numbers, and things like Esc or Del, and the arrow-keys.

So far I have been using a 3rd party module called readchar. A few approaches to the task are discussed here: How to read a single character from the user?. They run along these lines:

import termios, sys, tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
    tty.setraw(fd)
    ch = sys.stdin.read(1)
finally:
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

Unfortunately this only returns a single character such as x1b whereas an arrow key will be multiple characters such as x1bx5bx41. The module readchar tries to solve this problem by providing a function readkey which reads characters until is has read a complete key and then returns this. The problem is that it will hang when the Esc key is pressed because it is anticipating more characters.

How can I write a function which will immediately return the key which was pressed whether it be Esc or Del or something else?

Advertisement

Answer

If you are doing this in a terminal, you will need custom logic in your code to handle it.

For background: as far as your program knows, it may be connected to an old-school hardware terminal with a 300 bit-per-second transmission rate. That terminal may have an up-arrow key that automatically sends the characters ^[, [, A when up arrow is pressed, or it may not even have an up arrow, in which case the user has to press those three key combinations in sequence to get the up-arrow behavior (^+[ and esc both send the ^[ character). Or it could be a different model of terminal that sends a different sequence of characters when up arrow is pressed. If you use input() in Python, the operating system takes care of reading from stdin, identifying the remote terminal (or the terminal being emulated by the user’s terminal app), and translating these sequences into appropriate editing actions on the command line. But if you read from stdin directly, you will get some arbitrary sequence of characters whenever the user presses a special key (and it could vary depending on which model of terminal they are emulating).

In my terminal, the up arrow sends the character sequence ^[ [ A (you can check this by running cat, then pressing keys and seeing what shows up). So if you are reading the terminal character by character, there’s no way to tell whether the user has just pressed esc or whether they pressed some other key that issues a sequence of characters.

Assuming you have a good way to translate character sequences back to keystrokes, one way to handle this would be to read everything currently available on sys.stdin (use sys.stdin.read() instead of sys.stdin.read(1)), then process everything you get in the buffer. Sometimes this will work, because the terminal will push all the characters into the buffer at the same time and your app will get all of them at the same time. But that is not 100% guaranteed — read can in principle break up the stream any way it feels like. And over a slow terminal connection, your app may well have time to process each character before the next pending one comes in.

You might be able to get around this by waiting for a certain quiet period with no new input, then processing whatever you have so far. But the length of the quiet period will depend on the connection speed, processor load, etc., so that’s iffy. Also, the longer the delay, the more reliable your program will be, but also the more laggy.

To some extent, you are trying to redefine what counts as input to your program. So maybe you should just go all the way with that? By this I mean, command-line apps don’t normally respond to an esc character on its own; instead they accept it and wait for whatever follows. So a user can type esc[, [, A as slowly as they please, and the application will process each one in turn and eventually decide that it has a complete sequence, and do the up arrow. If your app is supposed to respond to esc, then maybe you should just do that, and remember that most people’s terminals will send ^[ followed by a bunch of additional characters when they press up arrow. So you could throw away the other stuff if it’s not important or wait and act on the whole sequence like other terminal programs.

Another option may be to use the Python curses library to interact more directly with the terminal, but I’m not very familiar with that.

I think the summary version of this would be: it is unavoidable that stdin will give you a ^[ character whenever the user presses esc or various other terminal-specific keys. The only way to distinguish which key was pressed is to begin checking for additional characters after the ^[, probably allowing a brief delay for each character (maybe 0.1s, using select.select). If you get the sequence of characters corresponding to a particular key, you go with that. If there’s a long enough delay with no additional character, then you give up and process what you have so far (possibly just the ^[, possibly a partial escape sequence). If this isn’t good enough, then you’d need to use a library that interacts more directly with the local hardware.

The curses library provides these behaviors—timeout, assemble sequence, translate to generic keystroke using terminfo database. There may be some lighter weight solution but I haven’t seen it.

Just to complete the discussion: some terminals allow you to print an escape sequence to the console that will reprogram the function keys, including the arrow keys, to send a different character or sequence of characters. e.g. there is a DECPFK command for VT100 terminals and emulators. This could be simpler than using curses (just program the arrow keys to send < and > or whatever—anything other than ^[). But the sequence may vary from terminal to terminal, so you would need to look it up in the terminfo database, if it’s there at all. I doubt this is worth the trouble.

Some of these may provide useful background:

User contributions licensed under: CC BY-SA
9 People found this is helpful
Advertisement