Thursday, October 29, 2009

Recipe 21.3. Getting Input One Character at a Time










Recipe 21.3. Getting Input One Character at a Time





Problem


You're writing an interactive application or a terminal-based game. You want to read a
user's input from standard input a single character at a time.




Solution


Most Ruby installations on Unix come with the the Curses extension installed. If Curses has the features you want to write the rest of your program, the simplest solution is to use it.


This simple Curses program echoes every key you type to the top-left corner of the screen. It stops when you hit the escape key (\e).[1]

[1] This code will also work in irb, but it'll look strange because Curses will be fighting with irb for control of the screen.



#!/usr/bin/ruby -w
# curses_single_char_input.rb
require 'curses'
include Curses

# Setup: create a curses screen that doesn't echo its input.
init_screen
noecho

# Cleanup: restore the terminal settings when the program is exited or
# killed.
trap(0) { echo }

while (c = getch) != ?\e do
setpos(0,0)
addstr("You typed #{c.chr.inspect}")
end



If you don't want Curses to take over your program, you can use the HighLine library instead (available as the highline gem). It does its best to define a get_
character
method that will work on your system. The get_
character
method itself is private, but you can access it from within a call to ask:



require 'rubygems'
require 'highline/import'

while (c = ask('') { |q| q.character = true; q.echo = false }) != "\e" do
print "You typed #{c.inspect}"
end



Be careful; ask echoes a newline after every character it receives.[2] That's why I use a print statement in that example instead of puts.

[2] This actually happens at the end of HighLine.get_response, which is called by ask.


Of course, you can avoid this annoyance by hacking the HighLine class to make get_character public:



class HighLine
public :get_character
end
input = HighLine.new
while (c = input.get_
character) != ?\e do
puts "You typed #{c.chr.inspect}"
end





Discussion


This is a huge and complicated problem that (fortunately) is completely hidden by Curses and HighLine. Here's the problem: Unix systems know how to talk to a lot of historic and modern terminals. Each one has a different feature set and a different command language. HighLine (through the Termios library it uses on Unix) and Curses hide this complexity.


Windows doesn't have to deal with a lot of terminal types, but Windows programs don't usually read from standard input either (much less one character at a time). To do single-
character input on Windows, HighLine makes raw Windows API calls. Here's some code based on HighLine's, which you can use on Windows if you don't want to require HighLine:



require 'Win32API'

def getch
@getch ||= Win32API.new('crtdll', '_getch', [], 'L')
@getch.call
end

while (c = getch) != ?\e
puts "You typed #{c.chr.inspect}"
end



HighLine also has two definitions f get_character for Unix; you can copy one of these if you don't want to require HighLine. The most reliable implementation is fairly complicated, and requires the termios gem. But if you need to require the termios gem, you might as well require the highline gem as well, and use HighLine's implementation as is. So if you want to do single-character input on Unix without requiring any gems, you'll need to rely on the Unix command stty:



def getch
state = `stty -g`
begin
`stty raw -echo cbreak`
$stdin.getc
ensure
`stty #{state}`
end
end

while (c = getch) != ?\e
puts "You typed #{c.chr.inspect}"
end



All of the HighLine code is in the main highline.rb file; search for "get_character".




See Also


  • Recipe 21.5, "Setting Up and Tearing Down a Curses Program"

  • Recipe 21.8, "Changing Text Color"













No comments:

Post a Comment