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.