dialog

Dialog is a ruby gem for interfacing with the dialog(1) program. It does away with the manual command-line fiddling, allowing ruby programs operating in a commandline-environment to comfortably obtain user input. Ncurses dialogs the easy way!

Installation

Dialog is available as a ruby gem from rubyforge. Using RubyGems you can install dialog with the commandline:

gem install dialog

Note: The dialog gem does not include the dialog program or ncurses, you need to install these seperately (see below).

License

dialog is Copyright © 2006-2008 Martin Landers. It is free software, and may be redistributed under the terms of the GNU General Public License. See COPYING.


Ncurses and the dialog program

A simple user interface goes a long way in terms of making an application user friendly (so does a good commandline interface, but you’re already got that, haven’t you?). However, not all applications enjoy the luxury of living in a rich GUI environment. Running an X server (or any other fancy GUI) on an embedded platform may be overkill or not supported at all. Maybe you’re writing some software to get an X Server running in the first place, or constrained to a minimal bootable version of linux. Or, perhaps, you’re just plain fond of console tools.

Whatever the reason, if you’re in need of a simple “console GUI”, dialog(1) is the tool for the job. In fact, the ncurses(3) library is the tool for the job. However, pure ncurses is notoriously hard to use. It only offers primitive operations like moving the cursor or putting characters on the screen. Ncurses has no notion of “widgets”, like text-boxes or menus.

Enter dialog. dialog is a commandline wrapper for the ncurses library, that knows how to display simple dialog types. dialog has been designed with shell-scripts or scripting languages in mind. When dialog is invoked, it displays a single dialog (or a series of dialogs) and returns to the calling process, informing it of the selected options. Every aspect of a dialog’s appearance can controlled by commandline parameters (for details, check out the dialog man page in section 1). For example, this commandline displays everybody’s favourite Monty Python Yes/No-style question (from “Monthy Python and the Holy Grail”, that is):

dialog --yes-label "Blue" --no-label "Yellow" --yesno "What... is your favourite colour?" 0 0

While this is a huge improvement over manually “drawing” each dialog with ncurses primitives, the tasks of building commandlines for dialog and parsing its output still are complex and not very ruby-like. Hence, the dialog gem, which handles these tasks and offers a simple, yet powerful API for invoking dialog.

The dialog gem API

The dialog gem wraps each type of dialog (box type in dialog-lingo) supported by the dialog program into a class. The class will have the same name as the box type, with the first letter capitalized. So, the --yesno box type becomes the Dialog::Yesno class (all classes live in the Dialog module, to avoid namespace clutter). So, our example from above becomes:

require 'rubygems'
require 'dialog'

include Dialog

yn = Yesno.new do |d|
  d.text "What... is your favourite colour?"
  d.yes "Blue"
  d.no "Yellow"
end
yn.show!

Now, that’s better. Note, that in order to actually display the dialog, we need to invoke the show! method of the dialog object. This allows us to pre-initialize complex dialog objects and display them at a later time (or even multiple times througout an application’s lifetime).

But how do we know what our humble knight, er, user selected? By asking, of course! All dialog objects offer a number of predicate methods like ok?, yes? or no? that tell us the outcome of displaying the dialog. So, we could use the following code to tell if we need to toss the knight into the volcano:

if yn.yes?
  puts "Go on. Off you go."
else
  puts "auuuuuuuugh."
end

Obviously, calling these methods only makes sense after we’ve actually displayed the dialog. Let’s look at the interface in a bit more detail.

Initializing dialog objects

Before a dialog object can be displayed, it must be initialized. At the very least, we need to set a text for the dialog, using the text method, but we may want to set a number of other options, like the captions of the yes and no buttons, as we’ve done above.

The dialog gem supports two ways of initializing a dialog object. The block-style initialization seen above, and plain initialization through setter methods. In addition to per-dialog options, the gem also supports setting application-wide defaults for all dialogs or using a “template” to initialize a dialog’s options.

Before we look at this in more detail, we need to introduce one distinction. The dialog program supports two kinds of options, box options and common options. Box options are specific to the kind of dialog we wish to display. For example, a menu requires one or more choices whereas a file selection box needs an intial pathname. On the other hand, common options, like a background title (--backtitle) or wheter the dialog should be rendered with a shadow, are applicable for all, or at least more than one, dialog type.

Because box options are dialog-specific, they are set using specialized methods in the individual classes. For example, the choice method in Dialog::Menu adds all box options for a single menu choice in one go. Common options, on the other hand, can always be set using methods of the same name as the option, with hyphens replaced by underscores (Check out the dialog(1) manpage for the options supported by your copy of the dialog program). So, for example, the --no-shadow option may be set by invoking the no_shadow method.

Yesno.new do |yn|
  yn.no_shadow
end

In fact, common options are not checked at all. So, if you know your version of the dialog program supports a special option called --hug-bunnies, taking two arguments, just call

Msgbox.new do |m|
  m.hug_bunnies "arg1" 2
end

to pass --hug-bunnies arg1 2 to dialog and be extra nice to our small furry friends. Note, however, that you need to be careful when using options changing the I/O-behaviour of dialog. You might break the gem.

A small number of convenience methods for setting the most frequently used common and box options is built-in to all dialog classes. This includes button handling methods like yes or cancel and the width, height and text methods.

Block-style initialization

In the examples so far, we’ve been using the block-style initialization for the dialog object. Block-style initialization is the preferred way of initializing a dialog instance as it keeps visual clutter at a minimum, clearly grouping together all option settings. Consider this more complex example:

Checklist.new do |c|
  c.backtitle "Indiana Jones and the Temple of Doom"
  c.defaultno
  c.width 60
  c.height 10
  c.title "Dinner menu"
  c.text "Dinner is ready, please choose your meal"
  c.choice "Critters", "Crispy Critters", :on
  c.choice "Snake", "Snake Surprise", :on
  c.choice "Monkey", "Iced Monkey Brain", :off
  c.ok "Serve it!"
  c.cancel "I'm not hungry"
  c.extra "Soup?"
end.show!

Setter-based initialization

Instead of using block-style initialization, we can also initialize dialog objects like any other plain object. This may be preferable when complex “computation” is needed between the initialization steps, or we can use this to change options of an already initialized dialog. Here’s the same example again, this time using setter-based initialization:

c = Checklist.new do
c.backtitle "Indiana Jones and the Temple of Doom"
c.defaultno
c.width 60
c.height 10
c.title "Dinner menu"
c.text "Dinner is ready, please choose your meal"
c.choice "Critters", "Crispy Critters", :on
c.choice "Snake", "Snake Surprise", :on
c.choice "Monkey", "Iced Monkey Brain", :off
c.ok "Serve it!"
c.cancel "I'm not hungry"
c.extra "Soup?"
c.show!

Setting default options

To avoid repeating ourselves (violating the DRY-principle) when different dialogs require common options, the dialog gem supports setting default options for all dialogs or a group of dialogs. It does this by virtue of the DialogOptions class. Each dialog instance contains an instance of this class, that is responsible for collecting the options of that dialog. In fact, all the fancy option-collecting methods explained above are courtesy of DialogOptions. The dialog instances just expose the interface (by delegating unknown methods to the DialogOptions instance) directly, to save us some typing.

Whenever you create an instance of a dialog class, the instance looks for a DialogOptions instance to clone. Unless you say otherwise, the application-wide default instance is copied. You can obtain this instance using the Dialog::default_options method. For reasons of consistency, the default_options method supports block-style initialization, just like the dialog objects.

So, supposing you are having more than one dialog dealing with the adventures of Indy in the Temple of Doom, you can set the backtitle for all of them using:

Dialog::default_options do |d|
  d.backtitle "Indiana Jones and the Temple of Doom"
end

Checklist.new do |c|
  # Will have backtitle set to the default established above
  c.defaultno
  c.width 60
  ...
end

Note, that you have to manipulate the default options instance before creating the dialogs (because the instance is cloned).

Setting default options goes a long way, but you can get even fancier and initialize (groups of) dialogs using your own DialogOptions templates. To do this, just create and configure DialogOptions instances, and pass them to the constructors of your dialogs. Note, that you have to specify a size for the dialog (the default is 0 for each dimension). For example:

indy2 = DialogOptions.new do |d|
  d.backtitle "Indiana Jones and the Temple of Doom"
end

indy3 = DialogOptions.new do |d|
  d.backtitle "Indiana Jones and the Last Crusade"
end

# Use indy2 as the template for the dialog's options
Checklist.new(0,0,indy2) do |c|
  c.title "Dinner menu"
end

Checklist.new(0,0,indy3) do |c|
  c.title "Select a grail to drink from"
end

Displaying a dialog object

As we’ve seem above, the simplest way to display a dialog is to invoke its show! method. The show! method forks off the dialog program (using the configured options) and blocks until it terminates, that is, until the user selects an option or the dialog times out. Let’s repeat our earlier example:

yn = Yesno.new do |d|
  d.text "What... is your favourite colour?"
  d.yes "Blue"
  d.no "Yellow"
end
yn.show!    # Will block until "something" happens

This is fine for most types of dialogs. What, however if we need to do some processing while the dialog is being displayed? For example, if we intend to use a Gauge (or some other dialog type) as a progress indicator. For these moments there is the show method. Like the show! method, show forks off the dialog program to display the dialog. It does not, however, block the caller. The dialog program executes in parallel with the main process. We can wait for the termination of the dialog program by using the wait method:

yn = Yesno.new do |d|
  d.text "What... is your favourite colour?"
  d.yes "Blue"
  d.no "Yellow"
end

yn.show
# Do some parallel processing
# ...  
yn.wait  # Wait for the dialog's completion

Note, that things like the predicate methods, etc. will only work after waiting for the dialog’s completion. Also note, that you must not invoke another dialog (using neither show nor show!) before having waited for the completion of the current one. A few dialog types (most notably Gauge and the Tailbox family) support updating the dialog. Let’s look at a complete (yet totally useless) example:

gauge = Gauge.new do |g|
  g.text "Please wait... magic in progress"
  g.complete 0.1
end
gauge.show

10.step(100, 10) do |v|
  gauge.complete v
  sleep 0.05
end
gauge.wait

First, we create a gauge and display it using the show method. This allows us to “process” the loop in parallel to displaying the dialog. In each step, we update the gauge’s status using the complete method. After we are finished, we make sure to wait for the termination of the dialog process by using the wait method.

Knowing what the user selected

When using a more complex dialog type (a menu for example) it is not enough to know which button the user pressed. We also need to know which menu item was selected when the user exited the dialog. The dialog program outputs this information on standard output in a format specific to the individual dialog type. For example, when using a menu, dialog prints the tag of the selected menu item. Check out the man page of dialog(1) for information on the output formats.

After displaying a dialog, the output of the dialog program is available using the output method. Let’s turn our Monty Python example into a menu to demonstrate this:

menu = Menu.new do |m|
  m.text "What... is your favourite colour?"
  m.choice "blue", "Blue"
  m.choice "yellow", "Yellow"
end
menu.show!

if menu.yes? && menu.output == "blue"
  puts "Go on. Off you go."
else
  puts "auuuuuuuugh."
  exit
end

Right now, the dialog gem does not offer help in parsing the output. It may do so in future versions.

Using blocks a event handlers

Okay, we know how to initialize and display dialogs and how to find out what the user selected. What’s more? A more ruby-like way of dealing with user input! Using lots of if-statements checking predicates like yes? to attach program logic to user input is awkward.

As a more ruby-like way of doing things, methods like yes, no, ok, cancel (all methods for defining dialog buttons) accept an optional block. This block is invoked when the user exits the dialog by selecting the button the handler is attached to. The block is given two arguments: The dialog object that invoked the block and, as a convenience, the dialog’s output. So our example from above becomes:

menu = Menu.new do |m|
  m.text "What... is your favourite colour?"
  m.choice "blue", "Blue"
  m.choice "yellow", "Yellow"

  m.no { |dialog,out| puts "auuuuuuuugh." }
  m.yes do |dialog,out|
    if out == "blue"
      puts "Go on. Off you go."
    else
      puts "auuuuuuuugh."
      exit
    end
  end
end
menu.show!

Availability and Portability

Both the dialog program and the ncurses library are installed by default on a number of Linux distributions. A number of similar solutions (like newt & whiptail) exist, but, arguably, ncurses and dialog are the most widely supported tools. If dialog and/or ncurses are not installed on your system, they usually can be using your favourite package manager, or built from source.

Both ncurses and dialog have successfully been ported to a large number of systems, including BSD, MacOS X and Windows. Note, however, that the dialog gem has only been tested on Linux so far and will most likely fail on non-Unix systems. The reason will probably be some small issue with wrong pathnames or similar. Please report a bug (see below) if the dialog gem fails to invoke the dialog program on your platform.

For more information, check out the homepages of ncurses and dialog, especially the ncurses FAQ

Known bugs

No unit tests.

Support (aka Reporting bugs)

The dialog homepage is dialog.rubyforge.org. You can download all releases of dialog from there, including the trunk from subversion. Please report bugs or requests for enhancement to dialog’s rubyforge tracker. If possible, please submit patches. Patches should be generated by “diff -u” or “svn diff” and state against which release/svn revision they were made.