Read a single keyboard hit (even arrows and special keys) without echoing, in Ruby
Alec Jacobson
July 31, 2009
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.getc
s 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.getc
s. If a special key like an arrow is pressed the extra STDIN.getc
s 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.