Read a single keyboard hit (even arrows and special keys) without echoing, in Ruby

Alec Jacobson

July 31, 2009

weblog/

I'm writing a command line program in ruby which interacts using single keystrokes with out hitting enter/return. Using system calls to stty I've been able to disable echoing (so the key you press isn't displayed on the screen) and enable raw buffering (so you don't have to hit enter/^D after the key to make it register into STDIN). Using this with STDIN.getc works fine for all keys on the keyboard that have single character equivalents: letters, numbers, punctuation, symbols, and white space (oddly enough it does work for the ESCAPE key, but more on that one later). This method does not work for special keys like the arrows or control keys (as in ^X or CNTRL+X) because those keys all register as a string of characters, not just a single character.
To read these "long" key strokes I make two extra calls to STDIN.getc if I see a special key coming (based on the first call to STDIN.getc. Now I have all the arrows and others reading fine, but unfortunately the ESCAPE key is only a single character key hit and that character is exactly the special first character I match to know that a long character has been pressed. My primitive solution is to place the extra STDIN.getcs in a thread and kill that thread almost immediately after it is run. This way if the ESCAPE key is pressed, the program won't wait on the extra STDIN.getcs. If a special key like an arrow is pressed the extra STDIN.getcs occur immediately and before the thread is killed. My read_char method:
# read a character without pressing enter and without printing to the screen
def read_char
  begin
    # save previous state of stty
    old_state = `stty -g`
    # disable echoing and enable raw (not having to press enter)
    system "stty raw -echo"
    c = STDIN.getc.chr
    # gather next two characters of special keys
    if(c=="\e")
      extra_thread = Thread.new{
        c = c + STDIN.getc.chr
        c = c + STDIN.getc.chr
      }
      # wait just long enough for special keys to get swallowed
      extra_thread.join(0.00001)
      # kill thread so not-so-long special keys don't wait on getc
      extra_thread.kill
    end
  rescue => ex
    puts "#{ex.class}: #{ex.message}"
    puts ex.backtrace
  ensure
    # restore previous state of stty
    system "stty #{old_state}"
  end
  return c
end
You may have to adjust the sleep time according to your machine. Here's a quickly case block to show what can be gathered:
# takes a single character command
def show_single_key
    c = read_char
	case c
	when " "
	  puts "SPACE"
	when "\t"
	  puts "TAB"
	when "\r"
	  puts "RETURN"
	when "\n"
	  puts "LINE FEED"
	when "\e"
	  puts "ESCAPE"
	when "\e[A"
	  puts "UP ARROW"
	when "\e[B"
	  puts "DOWN ARROW"
	when "\e[C"
	  puts "RIGHT ARROW"
	when "\e[D"
	  puts "LEFT ARROW"
	when "\177"
	  puts "BACKSPACE"
	when "\004"
	  puts "DELETE"
	when /^.$/
	  puts "SINGLE CHAR HIT: #{c.inspect}"
	else
	  puts "SOMETHING ELSE: #{c.inspect}"
	end
end
And here's nifty rubyism to run this indefinitely, say in irb:
show_single_key while(true)  
Note: It seems the HOME, END, PAGE UP and PAGE DOWN keys are stolen by the command line terminal and won't register to STDIN.getc. I don't know of a "fix" for this.
Note: I have no access to a Windows machine right now and would gladly appreciate a comment as to how much of this will work on Windows. Note: I have changed sleep(0.000000000000000000001) to the more ruby-friendly extra_thread.join(0.00001). I also decreased the limit. Neither of these really change anything in fact on my machine the code runs fine without this line.