A Weird Imagination

Handling keypresses in shell

The problem#

For certain applications it can be useful to get many quick responses from the computer, so we would like it to react to individual keypresses instead of requiring an entire command to be typed out and confirmed by pressing Enter. While this is a common feature of GUI and TUI toolkits (e.g., ncurses), it can also be useful for very lightweight inactive experiences written as simple shell scripts.

The solution#

read_keypress.sh will wait for the next keypress and return it as a string:

#!/bin/sh

STTY=$(stty --save)
stty raw -echo
SEL=$(dd if=/dev/tty bs=1k count=1 2>/dev/null)
stty "$STTY"
echo -n "$SEL"

The details#

Responding to a single keypress#

I included an earlier version of this script in a post a while ago:

#!/bin/sh

STTY=$(stty -g)
stty raw -echo
SEL=$(dd if=/dev/tty bs=1 count=1 2>/dev/null)
stty "$STTY"
echo "$SEL"

There's also StackOverflow answers pointing out a one-liner that effectively does the same thing:

read -rsn1 input

(The input can be omitted if you don't care what the keypress is and just want to wait for any key.)

But both of those have the problem that they explicitly read only a single byte, and it's easy to generate multi-byte keypresses. Hit the left arrow key (🠈) for example:

$ read -rsn1
$ [D

Reading the remaining bytes#

I initially tried to increase the count of bytes for dd to read:

SEL=$(dd if=/dev/tty bs=1 count=3 2>/dev/null)

But that made it block waiting for more keypresses if I did type a single byte key. I initially fixed that by leaving the original dd call and adding another dd doing a non-blocking read to get the rest of the bytes:

SEL=$(dd if=/dev/tty bs=1 count=1 2>/dev/null)
SEL="$SEL$(dd if=/dev/tty bs=1k count=1 iflag=nonblock 2>/dev/null)"

Then I realized that could be simplified to a single read of more bytes, which won't block waiting for more keypresses, but will read as much as it can:

SEL=$(dd if=/dev/tty bs=1k count=1 2>/dev/null)

How many bytes?#

That 1k is certainly overkill. No keypress is going to produce anywhere near that many bytes. But that's fine. I tried to figure out what an actual reasonable maximum is. I found someone else asking, but they didn't come to a clear conclusion. Maybe 32 bytes? Maybe more.

Getting the terminal in raw mode#

The stty command allows for setting various options about how the terminal behaves. The --save options saves all of those settings. Then we can safely clobber the settings by requesting "raw" mode, which disables interpreting any keypresses, leaving them to be read from /dev/tty. -echo additionally tells it to not print out everything that is typed. Then after reading the keypress from /dev/tty, we give the saved settings back to stty to restore the normal terminal behavior.

Avoiding the extra newline#

The simpler scripts given above have another minor issue that they add a newline (0x0a) byte at the end of the output. This doesn't actually matter that much in practice, but using echo -n avoids it.

Further reading#

See the read_keypress.sh tag for future posts about making use of this script.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.