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.
3 comments