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.
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
Thanks for your suggestion! I corrected the post.
Caching the instantiation of Win32API objects in constants is indeed a must-have.
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.