suuudooo
suuudooo

Reputation: 21

Bash arrow key detection in real-time

I'm trying to detect arrow key presses in real-time (i.e., without waiting for input to be buffered) in a Bash script. I set the terminal to raw mode and attempt to read the escape sequences for arrow keys.

The script mostly works, but when I hold down an arrow key for a couple of seconds, the script seems to hang and CPU utilization spikes. My attempt to flush the buffer doesn’t seem to resolve the issue.

Here is a minimal reproducible example:

#!/usr/bin/env bash

if [ -t 0 ]; then
  echo "We have a TTY on stdin."
else
  echo "No TTY on stdin—arrow key detection won't work properly."
  exit 1
fi

# Set terminal to raw mode so we can read arrow keys instantly
stty raw -echo

# Restore terminal settings on exit
cleanup() {
  stty sane
  echo "Exiting and restoring terminal."
}
trap cleanup EXIT

echo "Press arrow keys (or any key). Press Ctrl+C to exit."

while true; do
  # Attempt to read up to 3 bytes with a 0.1s timeout 
  # (Arrow keys typically send 3-byte escape sequences)
  if IFS= read -r -t 0.1 -n 3 keypress; then
    case "$keypress" in
      $'\e[A') echo "Up arrow!" ;;
      $'\e[B') echo "Down arrow!" ;;
      $'\e[C') echo "Right arrow!" ;;
      $'\e[D') echo "Left arrow!" ;;
      *)       echo "Pressed: ${keypress} (not an arrow)" ;;
    esac
  else
    # Nothing pressed in last 0.1s
    :
  fi

  # Clear any other input waiting in the buffer
  while IFS= read -r -t 0 -n 100000 _trash; do :; done

  # Sleep a bit before checking again
  sleep 0.2
done

I would like to know:

  1. Why does holding down an arrow key for a couple of seconds cause the script to hang and increase CPU usage?
  2. Is there a better way to detect arrow key presses in real time without causing high CPU usage?
  3. How can I properly flush the input buffer to handle long key presses?

Any guidance or best practices on reading raw keypresses in Bash would be greatly appreciated.

Upvotes: 2

Views: 55

Answers (1)

Philippe
Philippe

Reputation: 26727

Reading one character a time works better:

local state=0
while true; do
    if IFS= read -r -t .1 -n 1 c; then
        test "$c" = q && break
        if test "$state" = 0 -a "$c" = $'\e'; then
            state=1
        elif test "$state" = 1 -a "$c" = '['; then
            state=2
        else
            if test "$state" = 2; then
                case "$c" in
                A) echo "Up";;
                B) echo "Down";;
                C) echo "Right";;
                D) echo "Left";;
                esac
            fi  
            state=0
        fi  
    fi  
done

Upvotes: 3

Related Questions