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