How to read one non-blocking key press in Ruby

During the development of a simple command line game in Ruby, I wanted to check if the player has pressed a given key in a non-blocking and buffered way. That is:

  • If no key was pressed, don’t wait for one to be pressed before continuing the program execution.
  • If the user has pressed and released a key before getting it, the method has to still get it.
  • The buffer should not wait for a \n character to be input before giving previous characters.
  • I want the ASCII code of the key for non printable characters such as ESC.

As usual, I hate it when my Ruby programs are not cross-platforms, so I wanted a working solution for both Unix and Windows.
… aaand it was more complicated than I thought.

First, there are no Ruby cross-platform way to do it. There are 2 Unix ways and 2 Windows ways.
I then wrote a module using those ways to get a cross-platform solution. See at the end!

Unix

The first Unix way I found calls the stty Unix command:

system('stty raw -echo') # => Raw mode, no echo
char = (STDIN.read_nonblock(1).ord rescue nil)
system('stty -raw echo') # => Reset terminal mode

The second Unix way I found uses the curses library:

require 'curses'
Curses.timeout = 0
char = Curses.getch
char = char.ord if char

Windows

The first Windows way calls the Win32 API:

require 'Win32API'
char = (Win32API.new('crtdll', '_kbhit', [ ], 'I').Call.zero? ? nil : Win32API.new('crtdll', '_getch', [ ], 'L').Call)

The second Windows way also uses the curses library, but with an ugly tweak on the LINES environment variable:

require 'curses'
ENV['LINES'] = '40' # Needed for curses to work in a Windows command line
Curses.timeout = 0
char = Curses.getch
char = char.ord if char

This solution wreaks havoc a bit in the command line display, but still works given my requirements.

Cross-platform solution

Based on this code, I wrote the following module to get a cross-platform solution, without using curses:

module GetKey

# Check if Win32API is accessible or not
USE_STTY = begin
require 'Win32API'
KBHIT = Win32API.new('crtdll', '_kbhit', [ ], 'I')
GETCH = Win32API.new('crtdll', '_getch', [ ], 'L')
false
rescue LoadError
# Use Unix way
true
end

# Return the ASCII code last key pressed, or nil if none
#
# Return::
# * _Integer_: ASCII code of the last key pressed, or nil if none
def self.getkey
if USE_STTY
char = nil
begin
system('stty raw -echo') # => Raw mode, no echo
char = (STDIN.read_nonblock(1).ord rescue nil)
ensure
system('stty -raw echo') # => Reset terminal mode
end
return char
else
return KBHIT.Call.zero? ? nil : GETCH.Call
end
end

end

[Edit]: Improved thanks to Vlad’s suggestion!

The following program can easily test this new module:

loop do
k = GetKey.getkey
puts "Key pressed: #{k.inspect}"
sleep 1
end

And here is the output while typing “Hello!”:

Key pressed: nil
Key pressed: nil
Key pressed: nil
Key pressed: nil
Key pressed: 72
Key pressed: nil
Key pressed: 101
Key pressed: nil
Key pressed: 108
Key pressed: 108
Key pressed: 111
Key pressed: 33
Key pressed: nil
Key pressed: nil
Key pressed: nil
Key pressed: nil

It works both on Unix and Windows 😀 Woot!

Hope it will help.

About Muriel Salvan

I am a freelance project manager and polyglot developer, expert in Ruby and Rails. I created X-Aeon Solutions and rivierarb Ruby meetups. I also give trainings and conferences on technical topics. My core development principles: Plugins-oriented architectures, simple components, Open Source power, clever automation, constant technology watch, quality and optimized code. My experience includes big and small companies. I embrace agile methodologies and test driven development, without giving up on planning and risks containment methods as well. I love Open Source and became a big advocate.
Howto, Ruby , , , , , , , , , , , , , , , , , , , , ,

3 comments


Leave a Reply

Your email address will not be published.