Ruby has libraries for attaching programs to the three main types of user interface. The web interface, Ruby’s most popular, is covered in depth in Chapters 15, 16, and (to a lesser extent) 14. This chapter covers the other two interfaces: the terminal or console interface, and the graphical ( GUI) interface. We also cover some unorthodox interfaces (Recipe 21.11).
The terminal interface is is a text-based interface usually
invoked from a command line. It’s used by programs like irb
and the Ruby interpreter itself. The
terminal interface is usually seen on Unix systems, but all modern
operating systems support it.
In the classic Unix-style “command-line program,” the user interface
consists of the options used to invoke the program (Recipe 21.3); and the program’s
standard input, output, and error streams (Recipe 21.1; also see Recipe 6.16). The Ruby interpreter
is a good example of this kind of program. You can invoke the ruby
program with arguments like -d
and --version
, but once the interpreter starts, your
options are limited to typing in a Ruby program and executing it.
The advantage of this simple interface is that you can use Unix
shell tools like redirection and pipes to connect these programs to each
other. Instead of manually typing a Ruby program into the interpreter’s
standard input, you can send it a file with the Unix command ruby < file.rb
. If you’ve got another program
that generates Ruby code and prints it to standard output, you can pipe
the generated code into the interpreter with generator | ruby
.
The disadvantage is that these programs are not very user-friendly.
Libraries like Curses (Recipe
21.5), Readline, and HighLine can add color and sophistication to
your terminal programs. The irb
interactive interpreter uses Readline to offer interactive line editing
instead of the simpler interface offered by the Unix shell (Recipe 21.10).
The graphical user interface is the most common interface in the world. Even a web interface is usually interpreted within a GUI on the client end. However, there’s not much that’s Ruby-specific about GUI programming. All the common GUI libraries (like Tk, GTK, and QT) are written in C, and Ruby’s bindings to them look a lot like the bindings for other dynamic languages such as Perl and Python.
All the GUI libraries work pretty much the same way. You create objects corresponding to GUI elements, or “widgets,” attach chunks of code to them as callbacks (so that something will happen when, for instance, the user clicks a button), and then “pack” them into a frame for display. Because it’s easiest to do the GUI layout work in a tool like Glade, and write only the callbacks in regular Ruby, this chapter contains only a few sample recipes on GUI programming.
HighLine, written by James Edward Gray II and Gregory Brown, is
available as the highline
gem. The
Curses and Readline libraries come preinstalled with Ruby (even on
Windows, if you use the one-click installer). If you’re using Windows
and don’t have Curses, you can get the library and the Ruby bindings
from http://www.dave.burt.id.au/ruby/curses.zip.
Ncurses is an improved version of Curses (allowing things like
colored text), and most modern Unix systems have it installed. You can
get Ncurses bindings for Ruby from http://ncurses-ruby.berlios.de/. It’s also available as
the Debian package libncurses-ruby
.
The Tk binding for Ruby comes preinstalled with Ruby, assuming you’ve installed Tk itself. Ruby bindings for the most common GUI toolkits have been written:
wxRuby is interesting because it’s cross-platform and uses native widgets on each platform. You can write a Ruby program with wxRuby that runs on Unix, Windows, and Mac OS X, and looks like a native application on all three platforms.
On Mac OS X, all the tools you need to build a Ruby GUI application come with the operating system, including a GUI builder. If you’re using GTK, your life will be easier if you download the Glade GUI builder (http://glade.gnome.org/).
You’re writing an interactive console program, and you want to get line-based input from the user. You present the user with a prompt, and he types some data before hitting enter.
Instead of reading standard input all at once, read it a line at a time with
gets
or readline
.
This method populates a data structure with values obtained from user input:
def confirmation_hearings questions = [['What is your name?', :name], ['How old are you?', :age], ['Why would you like to be Secretary of the Treasury?', :why]] answers = questions.inject({}) do |answers, qv| question, value = qv print question + ' ' answers[value] = gets.chomp answers end puts "Okay, you're confirmed!" return answers end confirmation_hearings # What is your name? # <= Leonard Richardson # How old are you? # <= 27 # Why would you like to be Secretary of the Treasury? # <= Mainly for the money # Okay, you're confirmed! # => {:age=>"26", :why=>"Mainly for the money", :name=>"Leonard Richardson"}
Most console programs take their input from command-line
switches or from a file passed in on standard input. This makes it
easy to programatically combine console programs: you can pipe
cat
into grep
into last
without any of the programs having to
know that they’re connected to each other. But sometimes it’s more
user-friendly to ask for input interactively: in text-based games, or
data entry programs with workflow.
The only difference between this technique and traditional console applications is that you’re writing to standard output before you’re completely done reading from standard input. You can pass an input file into a program like this, and it’ll still work. In this example, a Ruby program containing the questionnaire code seen in the Solution is fed by an input file:
$ ./confirmation_hearings.rb < answers # => What is your name? How old are you? Why would you like to be # Secretary of the Treasury? Okay, you're confirmed!
The program works, but the result looks different—even though the standard output is actually the same. When a human is running the program, the newline created when they hit enter is echoed to the screen, making the second question appear on a separate line from the first. Those newlines don’t get echoed when they’re read from a file.
The HighLine library requires that you install a gem
( highline
), but it makes sophisticated
line-oriented input much easier. You can make a single method call to
print a prompt, retrieve the input, and validate it. This code works
the same way as the code above, but it’s shorter, and it makes sure
you enter a reasonable age for the question “How old are you?”
require 'rubygems' require 'highline/import' def confirmation_hearings answers = {} answers[:name] = ask('What is your name? ') answers[:age] = ask('How old are you? ', Integer) { |q| q.in = 0..120 } answers[:why] = ask('Why would you like to be Secretary of the Treasury? ') puts "Okay, you're confirmed!" return answers end confirmation_hearings # What is your name? # <= Leonard Richardson # How old are you? # <= twenty-seven # You must enter a valid Integer. # ? # <= 200 # Your answer isn't within the expected range (included in 0..120) # ? # <= 27 # …
Recipe 21.2, “Getting Input One Character at a Time”
Recipe 21.9, “Reading a Password”
The examples/basic_usage.rb
script in the
HighLine library has many more examples of data validation with
HighLine
If you want your program to treat its command-line arguments as filenames and read from the files one line at a time, see Recipe 21.3, “Parsing Command-Line Arguments,” for a shortcut
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.
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]
#!/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
.
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
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”.
Recipe 21.5, “Setting Up and Tearing Down a Curses Program”
Recipe 21.8, “Changing Text Color”
You want to make your Ruby script take command-line arguments, the way most Unix utilities and scripts do.
If you want to treat your command-line arguments as a simple
list of strings, you can just iterate over the ARGV
array.
Here’s a Ruby version of the Unix command cat
; it takes a list of files on the command
line, opens each one, and prints its contents to standard
output:
#!/usr/bin/ruby -w # cat.rb ARGV.each { |filename| IO.readlines(filename).each { |line| puts line } }
If you want to treat your command-line arguments as a list of
files, and you plan to open each of those files
and iterate over them line by line, you can use ARGF
instead of eARGV
. The following cat
implementation is equivalent to the
first one.[3]
#!/usr/bin/ruby -w # cat_argf.rb ARGF.each { |line| puts line }
If you want to treat certain command-line arguments as switches,
or as anything other than a homogenous list of strings, use the
OptionParser
class in the optparse
library. Don’t write the argument
parsing code yourself; there are too many edge cases to think
about.
The OptionParser
class can
parse any command-line arguments you’re likely to need, and it
includes a lot of Unix know-how that would take a long time to write
yourself. All you have to do is define the set of arguments your
script accepts, and write code that reacts to the presence of each
argument on the command line. Here, I’ll use OptionParser
to write cat2.rb
, a second Ruby version of cat
that supports a few of the real cat's
command-line arguments.
The first phase is turning any command-line arguments into a data structure that I can easily
consult during the actual program. The CatArguments
class defined below is a hash
that uses OptionParser
to populate
itself from a list of command-line arguments.
For each argument accepted by cat2.rb
, I’ve added a code block to be run
as a callback. When OptionParser
sees a particular argument in ARGV
,
it runs the corresponding code block, which sets an appropriate value
in the hash:
#!/usr/bin/ruby # cat2.rb require 'optparse' class CatArguments < Hash def initialize(args) super() self[:show_ends] = '' opts = OptionParser.new do |opts| opts.banner = "Usage: #$0 [options]" opts.on('-E', '--show-ends [STRING]', 'display [STRING] at end of each line') do |string| self[:show_ends] = string || '$' end opts.on('-n', '--number', 'number all output lines') do self[:number_lines] = true end opts.on_tail('-h', '--help', 'display this help and exit') do puts opts exit end end opts.parse!(args) end end arguments = CatArguments.new(ARGV)
At this point in the code, our CatArguments
object contains information
about which command-line arguments were passed in. If the user passed
in a command-line switch -E
or
--show-ends
, then
arguments[:show_ends] contains a string to be shown at the end of each
line.
What’s more, the command-line arguments handled by OptionParser
have been stripped from
ARGV
. The only things left in
ARGV
can be assumed to be the names
of files the user wants to concatenate. This means we can now use the
ARGF
shortcut to iterate over those
files line by line. All we need is a little extra code to actually
implement the command-line arguments:
counter = 0 eol = ARGF.each do |line| line.sub!(/$/, arguments[:show_ends]) print '%6.d ' % (counter += 1) if arguments[:number_lines] print line end
Here’s a shell session showing off the robustness that optparse
brings to even a simple script. The
help message is automatically generated, multiple combined flags are
handled correctly, nonexistent flags are rejected, and you can disable
flag processing altogether with the --
argument. In general, it works like you
expect a Unix command-line tool to work.
$ ./cat2.rb --help Usage: ./cat2.rb [options] -E, --show-ends [STRING] display STRING at end of each line -n, --number number all output lines -h, --help display this help and exit $ ./cat2.rb file1 file2 This is file one. Another line in file one. This is file two. I'm a lot more interesting than file one, I'll tell you that! $ ./cat2.rb file1 -E$ -n file2 1 This is file one.$ 2 Another line in file one.$ 3 This is file two.$ 4 I'm a lot more interesting than file one, I'll tell you that!$ $ ./cat2.rb --nosuchargument /usr/lib/ruby/1.8/optparse.rb:1445:in `complete': invalid option: --nosuchargument (OptionParser::InvalidOption) $ ./cat2.rb --show-ends=" STOP" -- --argument-looking-file The name of this file STOP looks just like an argument STOP for some odd reason. STOP
With a little more work, you can make OptionParser
validate argument data for
you—parse strings as numbers, restrict option values to values from a
list. The documentation for the OptionParser
class has a much more complex
example that shows off these advanced features.
ri OptionParser
You want to see whether there’s another person on the other end of your program, or whether the program has been hooked up to a file or the output of another program.
STDIN.tty?
returns true if
there’s a terminal hooked up to your program’s original standard
input. Since only humans use terminals, this will suffice. This code
works on Unix and Windows:
#!/usr/bin/ruby -w # interactive_or_not.rb if STDIN.tty? puts "Let me be the first to welcome my human overlords." else puts "How goes the revolution, brother software?" end
Running this program in different ways gives different results:
$ ./interactive_or_not.rb Let me be the first to welcome my human overlords. $ echo "Some data" | interactive_or_not.rb How goes the revolution, brother software? $ ./interactive_or_not.rb < input_file How goes the revolution, brother software?
An interactive application can be more user friendly than one
that runs solely off its command-line arguments and input streams. By
checking STDIN.tty?
you can make
your program have an interactive and a noninteractive mode. The
noninteractive mode can be chained together with other programs or
used in shell scripts.
To write a program that uses Curses or Ncurses, you have to write a lot of setup and cleanup code. You’d like to factor that out.
Here’s a wrapper method that sets up the Curses library and passes the main screen object into a code block:
require 'curses' module Curses def self.program main_screen = init_screen noecho cbreak curs_set(0) main_screen.keypad = true yield main_screen end end
Here’s a simple Ruby program that uses the wrapper method to fill up the screen with random placements of a given string:
Curses.program do |scr| str = ARGV[0] || 'Test' max_x = scr.maxx-str.size+1 max_y = scr.maxy 100.times do scr.setpos(rand(max_y), rand(max_x)) scr.addstr(str) end scr.getch end
The initialization, which is hidden in Curses.program
, does the following
things:
Stops keystrokes from being echoed to the screen (noecho
)
Hides the cursor (curs_set(0)
)
Turns off buffered input so keys can be processed as they’re
typed (cbreak
)
Makes the keyboard’s arrow keys generate recognizable key
events (keypad=true
)
The code is a little different if you’re using the third-party
ncurses
binding instead of the
curses
library that comes with
Ruby. The main difference is that with ncurses
, you must write some of the
cleanup code that the curses
library handles automatically. A
wrapper method is also a good place to set up the ncurses
color code if you plan to use
colored text (see Recipe
21.8 for more on this).
Here’s an Ncurses.program
method that’s equivalent to Curses.program
, except that it performs its
cleanup manually by registering an at_exit
block to run just before the
interpreter exits. This wrapper also turns on color and initializes a
few default color pairs. If your terminal has no color support, the
color code will run but it won’t do anything.
require 'ncurses' module Ncurses COLORS = [COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN, COLOR_WHITE] def self.program stdscr = Ncurses.initscr # Run ncurses cleanup code when the program exits. at_exit do echo nocbreak curs_set(1) stdscr.keypad(0) endwin end noecho cbreak curs_set(0) stdscr.keypad(1) start_color COLORS[1…COLORS.size].each_with_index do |color, i| init_pair(i+1, color, COLOR_BLACK) end yield stdscr end end
Here’s the ncurses
equivalent
of the curses
program given
earlier:
Ncurses.program do |scr| str = ARGV[0] || 'Test' max_y, max_x = [], [] scr.getmaxyx(max_y, max_x) max_y = max_y[0] max_x = max_x[0] - str.size + 1 100.times do scr.mvaddstr(rand(max_y), rand(max_x), str) end scr.getch end
See this chapter’s introduction for information on installing Ncurses
“Writing Programs with NCURSES” is a good general overview of the Ncurses library; it’s written for C programmers, but it’s useful for Rubyists because Ruby’s interfaces to Curses and Ncurses are little more than wrappers (http://dickey.his.com/ncurses/ncurses-intro.html)
You’re writing a console application, and you want it to clear the screen.
Capture the output of the Unix clear
command as a string and print it
whenever you want to clear the screen:
#!/usr/bin/ruby -w # clear_console.rb clear_code = %x{clear} puts 'Press enter to clear the screen.' $stdin.gets print clear_code puts "It's cleared!"
The clear
command prints an
escape code sequence to standard output, which the Unix terminal
interprets as a clear-screen command. The exact string depends on your
terminal, but it’s probably an ANSI escape sequence, like this:
%x{clear} # => "e[He[2J"
Your Ruby script can print this escape code sequence to standard
output, just as the clear
command
can, and clear the screen.
On Windows, the command is cls
, and you can’t just print its standard
output to clear the screen. Every time you want to clear the screen,
you need to call out to cls
with
Kernel#system
:
# clear_console_windows.rb puts 'Press enter to clear the screen.' $stdin.gets system('cls') puts "It's cleared!"
If you’ve made your Windows terminal support ANSI (see Recipe 21.8), then you can print the same ANSI escape sequence used on Unix.
The Curses library makes this a lot more straightforward. A
Curses application can clear any of its windows with Curses::Window#clear
. Curses::clear
will clear the main
window:
#!/usr/bin/ruby -w # curses_clear.rb require 'curses' Curses.init_ screen Curses.setpos(0,0) Curses::addstr("Type all you want. 'C' clears the screen, Escape quits. ") begin c = nil begin c = Curses.getch end until c == ?C or c == ?e Curses.clear end until c == ?e
But, as always, Curses takes over your whole application, so you might want to just use the escape sequence trick.
Within a terminal-based application, you want to find the size of the terminal: how many rows and columns are available for you to draw on.
This is easy if you’re using the Curses library. This example
uses the Curses.program
wrapper
described in Recipe
21.5:
Curses.program do |scr| max_y, max_x = scr.maxy, scr.maxx scr.setpos(0, 0) scr.addstr("Your terminal size is #{max_x}x#{max_y}. Press any key to exit.") scr.getch end
It’s a little less easy with Ncurses: you have to pass in two arrays to the underlying C libraries, and extract the numbers from the arrays. Again, this example uses the Ncurses wrapper from Recipe 21.5:
Ncurses.program do |scr| max_y, max_x = [], [] scr.getmaxyx(max_y, max_x) max_y, max_x = max_y[0], max_x[0] str = "Your terminal size is #{max_x}x#{max_y}. Press any key to exit." scr.mvaddstr(0, 0, str) scr.getch end
If you’re not using a Curses-style library, it’s not easy at all.
If you plan to simulate graphical elements on a textual terminal, subdivide it into virtual windows, or print justified output, you’ll need to know the terminal’s dimensions. For decades, the standard terminal size has been 25 rows by 80 columns, but modern GUIs and high screen resolutions let users create text terminals of almost any size. It’s okay to enforce a minimum terminal size, but it’s a bad idea to assume that the terminal is any specific size.
The terminal size is a very useful piece of information to have, but it’s not an easy one to get. The Curses library was written to solve this kind of problem, but if you’re willing to go into the operating system API, or if you’re on Windows where Curses is not a standard feature, you can find the terminal size without letting a Curses-style library take over your whole application.
On Unix systems (including Mac OS X), you can make an ioctl
system call to get the terminal size.
Since you’re calling out to the underlying operating system, you’ll
need to use strange constants and C-like structures to carry the
response:
TIOCGWINSZ = 0x5413 # For an Intel processor # TIOCGWINSZ = 0x40087468 # For a PowerPC processor def terminal_size rows, cols = 25, 80 buf = [ 0, 0, 0, 0 ].pack("SSSS") if STDOUT.ioctl(TIOCGWINSZ, buf) >= 0 then rows, cols, row_pixels, col_pixels = buf.unpack("SSSS")[0..1] end return rows, cols end terminal_size # => [21, 80]
Here, the methods pack
and
unpack
convert between a
four-element array and a string that is modified in-place by the
ioctl
call. After the call, the
first two elements of the array contain the number of rows and columns
for the terminal. Note that the first argument to ioctl
is architecture-dependent.
The Windows version works the same way, although you must jump through more hoops and the system call returns a much bigger data structure:
STDOUT_HANDLE = 0xFFFFFFF5 def terminal_size m_GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L') m_GetConsoleScreenBufferInfo = Win32API.new ('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L' ) format = 'SSSSSssssSS' buf = ([0] * format.size).pack(format) stdout_handle = m_GetStdHandle.call(STDOUT_HANDLE) m_GetConsoleScreenBufferInfo.call(stdout_handle, buf) (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = buf.unpack(format) return bottom - top + 1, right - left + 1 end terminal_size # => [25, 80]
If all else fails, on Unix systems you can call out to the
stty
command:
def terminal_size %x{stty size}.split.collect { |x| x.to_i } end terminal_size # => [21, 80]
The ioctl
code is based
on code posted to ruby-talk by Paul Brannan (http://blade.nagaokaut.ac.jp/cgi-bin/rcat.rb/ruby/ruby-talk/40350)
The Windows code is based on code in the Win32API_Console library, a simple Ruby wrapper around Windows’ console-related API calls (http://rb-w32mod.sourceforge.net/)
Recipe 21.5, “Setting Up and Tearing Down a Curses Program”
The simplest solution is to use HighLine. It lets you enclose color commands in an ERb template that gets interpreted within HighLine and printed to standard output. Try this colorful bit of code to test the capabilities of your terminal:
require 'rubygems' require 'highline/import' say(%{Here's some <%= color('dark red text', RED) %>.}) say(%{Here's some <%= color('bright red text on a blue background', RED+BOLD+ON_BLUE) %>.}) say(%{Here's some <%= color('blinking bright cyan text', CYAN+BOLD+BLINK) %>.}) say(%{Here's some <%= GREEN+UNDERLINE %>underlined dark green text<%=CLEAR%>.})
Some of these features (particularly the blinking and underlining) aren’t supported on all terminals.
The HighLine#color
method encloses a display
string in special command strings, which start with an escape
character and a left square bracket:
HighLine.new.color('Hello', HighLine::GREEN) # => "e[32mHelloe[0m"
These are ANSI escape sequences. Instead of displaying the string “e[32m”, an ANSI-compatible terminal treats it as a command: in this case, a command to start printing characters in green-on-black. The string “e[0m” tells the terminal to go back to white-on-black.
Most modern Unix terminals support ANSI escape sequences,
including the Mac OS X terminal. You should be able to get green text
in your irb
session just by calling
puts "e[32mHelloe[0m"
(try it!),
but HighLine makes it easy to get color without having to remember the
ANSI sequences.
Windows terminals don’t support ANSI by default, but you can get it to work by loading ANSI.SYS (see below for a relevant Microsoft support article).
An alternative to HighLine is the Ncurses library.[4] It supports color terminals that use a means other than ANSI, but these days, most color terminals get their color support through ANSI. Since Ncurses is much more complex than HighLine, and not available as a gem, you should only use Ncurses for color if you’re already using it for its other features.
Here’s a rough equivalent of the HighLine program given above.
This program uses the Ncurses::program
wrapper described in Recipe 21.5. The wrapper sets
up Ncurses and initializes some default color pairs:
Ncurses.program do |s| # Define the red-on-blue color pair used in the second string. # All the default color pairs use a black background. Ncurses.init_pair(8, Ncurses::COLOR_RED, Ncurses::COLOR_BLUE) Ncurses::attrset(Ncurses::COLOR_PAIR(1)) s.mvaddstr(0,0, "Here's some dark red text.") Ncurses::attrset(Ncurses::COLOR_PAIR(8) | Ncurses::A_BOLD) s.mvaddstr(1,0, "Here's some bright red text on a blue background.") Ncurses::attrset(Ncurses:: COLOR_PAIR(6) | Ncurses::A_BOLD | Ncurses::A_BLINK) s.mvaddstr(2,0, "Here's some blinking bright cyan text.") Ncurses::attrset(Ncurses::COLOR_PAIR(2) | Ncurses::A_UNDERLINE) s.mvaddstr(3,0, "Here's some underlined dark green text.") s.getch end
An Ncurses program can draw from a palette of color
pairs—combinations of foreground and background colors. Ncurses::program
sets up a default palette
of the seven basic ncurses
colors
(red, green, yellow, blue, magenta, cyan, and white), each on a black
background. You can change this around if you like, or define
additional color pairs (like the red-on-blue defined in the example).
The following Ncurses program prints out a color chart of all
foreground-background pairs. It makes the text of the chart bold, so
that the text doesn’t become invisible when the background is the same
color.
Ncurses.program do |s| pair = 0 Ncurses::COLORS.each_with_index do |background, i| Ncurses::COLORS.each_with_index do |foreground, j| Ncurses::init_pair(pair, foreground, background) unless pair == 0 Ncurses::attrset(Ncurses::COLOR_PAIR(pair) | Ncurses::A_BOLD) s.mvaddstr(i, j*4, "#{foreground},#{background}") pair += 1 end end s.getch end
You can modify a color pair by combining it with an Ncurses
constant. The most useful constants are Ncurses::A_BOLD, Ncurses::A_BLINK
, and
Ncurses::A_UNDERLINE
. This works
the same way (and, on an ANSI system, uses the same ANSI codes) as
HighLine’s BOLD, BLINK
, and
UNDERLINE
constants. The only
difference is that you modify an Ncurses color with the OR operator
(|
), and you modify a HighLine
color with the addition operator.
Recipe 1.3, “Substituting Variables into an Existing String,” has more on ERb
http://en.wikipedia.org/wiki/ANSI_escape_code has technical details on ANSI color codes
The examples/ansi_colors.rb
file in the
HighLine gem
You can get a set of Ncurses bindings for Ruby at http://ncurses-ruby.berlios.de/; it’s also
available as the Debian package libncurses-ruby
If you want something more lightweight than the highline
gem, try the termansicolor
gem instead: it defines
methods for generating the escape sequences for ANSI colors, and
nothing else
“How to Enable ANSI.SYS in a Command Window” (http://support.microsoft.com/?id=101875)
You want to prompt the user for a password, or otherwise capture input without echoing it to the screen for all to see.
The ruby-password
library makes this easy, but
it’s not available as a Ruby gem. The HighLine library is available as
a gem, and it can do this almost as well. You just have to turn off
the terminal echo feature:
require 'rubygems' require 'highline/import' def get_password(prompt='Password: ') ask(prompt) { |q| q.echo = false} end get_password("What's your password? ") # What's your password? # => "buddy"
In 2000, President Bill Clinton signed into law the Electronic Signatures Bill, which makes electronic signatures as binding as handwritten signatures. He signed the law by hand and then signed it electronically. As he typed the password to his electronic signature, it was was echoed to the screen. Everyone in the world saw that his password was the name of his pet dog, Buddy. Don’t let this happen to you: turn off echoing when gathering passwords.
Turning off echoing altogether is the safest way to gather a
password, but it might make your users think your program has stopped
responding to input. It’s more userfriendly to echo a mask character,
like an asterisk, for every character the user types. You can do this
in HighLine by setting echo
to the
mask character instead of false
:
def get_password(prompt='Password: ', mask='*') ask(prompt) { |q| q.echo = mask } end get_password # Password: ***** # => "buddy" get_password('Password: ', false) # Password: # => "buddy"
The ruby-password
third-party library also
provides ways of generating, encrypting, and test-cracking
passwords (http://www.caliban.org/ruby/ruby-password.shtml
)
You want to let your users edit their lines of input as they
write them, the way irb
does.
Use the readline
library.
Instead of reading directly from standard input, pass a prompt
string into Readline.readline
. The
user will be able to edit their input using the same shortcut keys you
can use in the irb
Ruby interpreter
(assuming their terminal supports those keys).
#!/usr/bin/ruby -w # readline.rb require 'readline' vegetable = Readline.readline("What's your favorite vegetable?> ") puts "#{vegetable.capitalize}? Are you crazy?"
Note that you don’t have to chomp
the result of Readline.readline
:
$ ruby readline.rb What's your favorite vegetable?> okra Okra? Are you crazy?
On Windows, this isn’t necessary because the cmd shell
provides any console program with
many of readline
’s features. The
example given above will work on both Windows and Unix, but if you’re
writing a Windows-specific program, you don’t need readline
:
# readline_windows.rb print "What's your favorite vegetable?> " puts gets.chomp.capitalize + "? Are you crazy?"