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


  1. Vlad


    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 of the 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

  2. Andy

    Hi. You probably need to rewrite your cross-platform variant using FFI because starting from Ruby 2.0 Win32API is deprecated, and while performing your code on Windows I face with problems around Win32API.new(…).Call which uses function “initialize”Win32API.rb, row 12.

    I also want to notice that by rescuing LoadError and sending your flow to Unix part you also get there other load errors (like it was with me), when the operating system is not Unix.

Leave a Reply

Your email address will not be published. Required fields are marked *