dialog 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,415 @@
1
+ == dialog
2
+
3
+ Dialog is a ruby gem for interfacing with the dialog(1) program. It does away
4
+ with the manual command-line fiddling, allowing ruby programs operating in a
5
+ commandline-environment to comfortably obtain user input. Ncurses dialogs the
6
+ easy way!
7
+
8
+ == Installation
9
+
10
+ Dialog is available as a ruby gem from rubyforge[http://www.rubyforge.org].
11
+ Using RubyGems[http://docs.rubygems.org/] you can install dialog with the
12
+ commandline:
13
+
14
+ gem install dialog
15
+
16
+ <em>Note: The dialog gem does not include the dialog program or ncurses, you
17
+ need to install these seperately</em> (see below).
18
+
19
+ == License
20
+
21
+ dialog is Copyright (c) 2006 Martin Landers, jambit.com. It is free software,
22
+ and may be redistributed under the terms of the GNU General Public License.
23
+ See COPYING.
24
+
25
+ ---
26
+
27
+ == Ncurses and the dialog program
28
+
29
+ A simple user interface goes a long way in terms of making an application user
30
+ friendly (so does a good commandline interface, but you're already got that,
31
+ haven't you?). However, not all applications enjoy the luxury of living in a
32
+ rich GUI environment. Running an X server (or any other fancy GUI) on an
33
+ embedded platform may be overkill or not supported at all. Maybe you're
34
+ writing some software to get an X Server running in the first place, or
35
+ constrained to a minimal bootable version of linux. Or, perhaps, you're just
36
+ plain fond of console tools.
37
+
38
+ Whatever the reason, if you're in need of a simple "console GUI",
39
+ dialog(1)[http://invisible-island.net/dialog/] is the tool for the job. In
40
+ fact, the ncurses(3)[http://invisible-island.net/ncurses/] library is the tool
41
+ for the job. However, pure ncurses is notoriously hard to use. It only offers
42
+ primitive operations like moving the cursor or putting characters on the
43
+ screen. Ncurses has no notion of "widgets", like text-boxes or menus.
44
+
45
+ Enter dialog. dialog is a commandline wrapper for the ncurses library,
46
+ that knows how to display simple dialog types. dialog has been designed
47
+ with shell-scripts or scripting languages in mind. When dialog is invoked, it
48
+ displays a single dialog (or a series of dialogs) and returns to the calling
49
+ process, informing it of the selected options. Every aspect of a dialog's
50
+ appearance can controlled by commandline parameters (for details, check out
51
+ the dialog man page in section 1). For example, this commandline displays
52
+ everybody's favourite Monty Python Yes/No-style question (from "Monthy Python
53
+ and the Holy Grail", that is):
54
+
55
+ dialog --yes-label "Blue" --no-label "Yellow" --yesno "What... is your favourite colour?" 0 0
56
+
57
+ While this is a huge improvement over manually "drawing" each dialog with
58
+ ncurses primitives, the tasks of building commandlines for dialog and parsing
59
+ its output still are complex and not very ruby-like. Hence, the dialog gem,
60
+ which handles these tasks and offers a simple, yet powerful API for invoking
61
+ dialog.
62
+
63
+ == The dialog gem API
64
+
65
+ The dialog gem wraps each type of dialog (<em>box type</em> in dialog-lingo)
66
+ supported by the dialog program into a class. The class will have the same
67
+ name as the box type, with the first letter capitalized. So, the
68
+ <tt>--yesno</tt> box type becomes the Dialog::Yesno class (all classes live
69
+ in the Dialog module, to avoid namespace clutter). So, our example from above
70
+ becomes:
71
+
72
+ require 'rubygems'
73
+ require 'dialog'
74
+
75
+ include Dialog
76
+
77
+ yn = Yesno.new do |d|
78
+ d.text "What... is your favourite colour?"
79
+ d.yes "Blue"
80
+ d.no "Yellow"
81
+ end
82
+ yn.show!
83
+
84
+ Now, that's better. Note, that in order to actually display the dialog, we
85
+ need to invoke the <tt>show!</tt> method of the dialog object. This allows us
86
+ to pre-initialize complex dialog objects and display them at a later time (or
87
+ even multiple times througout an application's lifetime).
88
+
89
+ But how do we know what our humble knight, er, user selected? By asking, of
90
+ course! All dialog objects offer a number of predicate methods like
91
+ <tt>ok?</tt>, <tt>yes?</tt> or <tt>no?</tt> that tell us the outcome of
92
+ displaying the dialog. So, we could use the following code to tell if we need
93
+ to toss the knight into the volcano:
94
+
95
+ if yn.yes?
96
+ puts "Go on. Off you go."
97
+ else
98
+ puts "auuuuuuuugh."
99
+ end
100
+
101
+ Obviously, calling these methods only makes sense after we've actually
102
+ displayed the dialog. Let's look at the interface in a bit more detail.
103
+
104
+ === Initializing dialog objects
105
+
106
+ Before a dialog object can be displayed, it must be initialized. At the very
107
+ least, we need to set a text for the dialog, using the +text+ method, but we
108
+ may want to set a number of other options, like the captions of the yes and no
109
+ buttons, as we've done above.
110
+
111
+ The dialog gem supports two ways of initializing a dialog object. The
112
+ block-style initialization seen above, and plain initialization through
113
+ setter methods. In addition to per-dialog options, the gem also supports
114
+ setting application-wide defaults for all dialogs or using a "template"
115
+ to initialize a dialog's options.
116
+
117
+ Before we look at this in more detail, we need to introduce one distinction.
118
+ The dialog program supports two kinds of options, <tt>box options</tt> and
119
+ <tt>common options</tt>. Box options are specific to the kind of dialog we
120
+ wish to display. For example, a menu requires one or more choices whereas a
121
+ file selection box needs an intial pathname. On the other hand, common
122
+ options, like a background title (<tt>--backtitle</tt>) or wheter the dialog
123
+ should be rendered with a shadow, are applicable for all, or at least more
124
+ than one, dialog type.
125
+
126
+ Because box options are dialog-specific, they are set using specialized
127
+ methods in the individual classes. For example, the +choice+ method in
128
+ Dialog::Menu adds all box options for a single menu choice in one go. Common
129
+ options, on the other hand, can always be set using methods of the same name
130
+ as the option, with hyphens replaced by underscores (Check out the dialog(1)
131
+ manpage for the options supported by your copy of the dialog program). So, for
132
+ example, the <tt>--no-shadow</tt> option may be set by invoking the
133
+ +no_shadow+ method.
134
+
135
+ Yesno.new do |yn|
136
+ yn.no_shadow
137
+ end
138
+
139
+ In fact, common options are not checked at all. So, if you know your version
140
+ of the dialog program supports a special option called <tt>--hug-bunnies</tt>,
141
+ taking two arguments, just call
142
+
143
+ Msgbox.new do |m|
144
+ m.hug_bunnies "arg1" 2
145
+ end
146
+
147
+ to pass <tt>--hug-bunnies arg1 2</tt> to dialog and be extra nice to our small
148
+ furry friends. Note, however, that you need to be careful when using options
149
+ changing the I/O-behaviour of dialog. You might break the gem.
150
+
151
+ A small number of convenience methods for setting the most frequently used
152
+ common and box options is built-in to all dialog classes. This includes button
153
+ handling methods like +yes+ or +cancel+ and the +width+, +height+ and +text+
154
+ methods.
155
+
156
+ ==== Block-style initialization
157
+
158
+ In the examples so far, we've been using the <em>block-style
159
+ initialization</em> for the dialog object. Block-style initialization is the
160
+ preferred way of initializing a dialog instance as it keeps visual clutter at
161
+ a minimum, clearly grouping together all option settings. Consider this more
162
+ complex example:
163
+
164
+ Checklist.new do |c|
165
+ c.backtitle "Indiana Jones and the Temple of Doom"
166
+ c.defaultno
167
+ c.width 60
168
+ c.height 10
169
+ c.title "Dinner menu"
170
+ c.text "Dinner is ready, please choose your meal"
171
+ c.choice "Critters", "Crispy Critters", :on
172
+ c.choice "Snake", "Snake Surprise", :on
173
+ c.choice "Monkey", "Iced Monkey Brain", :off
174
+ c.ok "Serve it!"
175
+ c.cancel "I'm not hungry"
176
+ c.extra "Soup?"
177
+ end.show!
178
+
179
+ ==== Setter-based initialization
180
+
181
+ Instead of using block-style initialization, we can also initialize dialog
182
+ objects like any other plain object. This may be preferable when complex
183
+ "computation" is needed between the initialization steps, or we can use this
184
+ to change options of an already initialized dialog. Here's the same example
185
+ again, this time using setter-based initialization:
186
+
187
+ c = Checklist.new do
188
+ c.backtitle "Indiana Jones and the Temple of Doom"
189
+ c.defaultno
190
+ c.width 60
191
+ c.height 10
192
+ c.title "Dinner menu"
193
+ c.text "Dinner is ready, please choose your meal"
194
+ c.choice "Critters", "Crispy Critters", :on
195
+ c.choice "Snake", "Snake Surprise", :on
196
+ c.choice "Monkey", "Iced Monkey Brain", :off
197
+ c.ok "Serve it!"
198
+ c.cancel "I'm not hungry"
199
+ c.extra "Soup?"
200
+ c.show!
201
+
202
+ ==== Setting default options
203
+
204
+ To avoid repeating ourselves (violating the DRY-principle) when different
205
+ dialogs require common options, the dialog gem supports setting default
206
+ options for all dialogs or a group of dialogs. It does this by virtue of the
207
+ DialogOptions class. Each dialog instance contains an instance of this class,
208
+ that is responsible for collecting the options of that dialog. In fact, all the
209
+ fancy option-collecting methods explained above are courtesy of DialogOptions.
210
+ The dialog instances just expose the interface (by delegating unknown methods
211
+ to the DialogOptions instance) directly, to save us some typing.
212
+
213
+ Whenever you create an instance of a dialog class, the instance looks for a
214
+ DialogOptions instance to clone. Unless you say otherwise, the
215
+ application-wide default instance is copied. You can obtain this instance
216
+ using the <tt>Dialog::default_options</tt> method. For reasons of consistency,
217
+ the +default_options+ method supports block-style initialization, just like
218
+ the dialog objects.
219
+
220
+ So, supposing you are having more than one dialog dealing with the adventures
221
+ of Indy in the Temple of Doom, you can set the backtitle for all of them
222
+ using:
223
+
224
+ Dialog::default_options do |d|
225
+ d.backtitle "Indiana Jones and the Temple of Doom"
226
+ end
227
+
228
+ Checklist.new do |c|
229
+ # Will have backtitle set to the default established above
230
+ c.defaultno
231
+ c.width 60
232
+ ...
233
+ end
234
+
235
+ Note, that you have to manipulate the default options instance _before_
236
+ creating the dialogs (because the instance is _cloned_).
237
+
238
+ Setting default options goes a long way, but you can get even fancier and
239
+ initialize (groups of) dialogs using your own +DialogOptions+ templates. To do
240
+ this, just create and configure +DialogOptions+ instances, and pass them to
241
+ the constructors of your dialogs. Note, that you have to specify a size for
242
+ the dialog (the default is 0 for each dimension). For example:
243
+
244
+ indy2 = DialogOptions.new do |d|
245
+ d.backtitle "Indiana Jones and the Temple of Doom"
246
+ end
247
+
248
+ indy3 = DialogOptions.new do |d|
249
+ d.backtitle "Indiana Jones and the Last Crusade"
250
+ end
251
+
252
+ # Use indy2 as the template for the dialog's options
253
+ Checklist.new(0,0,indy2) do |c|
254
+ c.title "Dinner menu"
255
+ end
256
+
257
+ Checklist.new(0,0,indy3) do |c|
258
+ c.title "Select a grail to drink from"
259
+ end
260
+
261
+ === Displaying a dialog object
262
+
263
+ As we've seem above, the simplest way to display a dialog is to invoke its
264
+ <tt>show!</tt> method. The <tt>show!</tt> method forks off the dialog program
265
+ (using the configured options) and blocks until it terminates, that is, until
266
+ the user selects an option or the dialog times out. Let's repeat our earlier
267
+ example:
268
+
269
+ yn = Yesno.new do |d|
270
+ d.text "What... is your favourite colour?"
271
+ d.yes "Blue"
272
+ d.no "Yellow"
273
+ end
274
+ yn.show! # Will block until "something" happens
275
+
276
+ This is fine for most types of dialogs. What, however if we need to do some
277
+ processing while the dialog is being displayed? For example, if we intend to
278
+ use a Gauge (or some other dialog type) as a progress indicator. For these
279
+ moments there is the +show+ method. Like the <tt>show!</tt> method, +show+
280
+ forks off the dialog program to display the dialog. It does not, however,
281
+ block the caller. The dialog program executes in parallel with the main
282
+ process. We can wait for the termination of the dialog program by using the
283
+ +wait+ method:
284
+
285
+ yn = Yesno.new do |d|
286
+ d.text "What... is your favourite colour?"
287
+ d.yes "Blue"
288
+ d.no "Yellow"
289
+ end
290
+
291
+ yn.show
292
+ # Do some parallel processing
293
+ # ...
294
+ yn.wait # Wait for the dialog's completion
295
+
296
+ Note, that things like the predicate methods, etc. will only work after
297
+ waiting for the dialog's completion. Also note, that you must not invoke
298
+ another dialog (using neither +show+ nor <tt>show!</tt>) before having waited
299
+ for the completion of the current one. A few dialog types (most notably
300
+ +Gauge+ and the +Tailbox+ family) support updating the dialog. Let's look at a
301
+ complete (yet totally useless) example:
302
+
303
+ gauge = Gauge.new do |g|
304
+ g.text "Please wait... magic in progress"
305
+ g.complete 0.1
306
+ end
307
+ gauge.show
308
+
309
+ 10.step(100, 10) do |v|
310
+ gauge.complete v
311
+ sleep 0.05
312
+ end
313
+ gauge.wait
314
+
315
+ First, we create a gauge and display it using the +show+ method. This allows
316
+ us to "process" the loop in parallel to displaying the dialog. In each step,
317
+ we update the gauge's status using the +complete+ method. After we are
318
+ finished, we make sure to wait for the termination of the dialog process by
319
+ using the +wait+ method.
320
+
321
+ === Knowing what the user selected
322
+
323
+ When using a more complex dialog type (a menu for example) it is not enough to
324
+ know which button the user pressed. We also need to know which menu item was
325
+ selected when the user exited the dialog. The dialog program outputs this
326
+ information on standard output in a format specific to the individual dialog
327
+ type. For example, when using a menu, dialog prints the tag of the selected
328
+ menu item. Check out the man page of dialog(1) for information on the output
329
+ formats.
330
+
331
+ After displaying a dialog, the output of the dialog program is available using
332
+ the +output+ method. Let's turn our Monty Python example into a menu to
333
+ demonstrate this:
334
+
335
+ menu = Menu.new do |m|
336
+ m.text "What... is your favourite colour?"
337
+ m.choice "blue", "Blue"
338
+ m.choice "yellow", "Yellow"
339
+ end
340
+ menu.show!
341
+
342
+ if menu.yes? && menu.output == "blue"
343
+ puts "Go on. Off you go."
344
+ else
345
+ puts "auuuuuuuugh."
346
+ exit
347
+ end
348
+
349
+ Right now, the dialog gem does not offer help in parsing the output. It may do
350
+ so in future versions.
351
+
352
+ === Using blocks a event handlers
353
+
354
+ Okay, we know how to initialize and display dialogs and how to find out what
355
+ the user selected. What's more? A more ruby-like way of dealing with user
356
+ input! Using lots of if-statements checking predicates like <tt>yes?</tt> to
357
+ attach program logic to user input is awkward.
358
+
359
+ As a more ruby-like way of doing things, methods like +yes+, +no+, +ok+,
360
+ +cancel+ (all methods for defining dialog buttons) accept an optional
361
+ block. This block is invoked when the user exits the dialog by selecting the
362
+ button the handler is attached to. The block is given two arguments: The
363
+ dialog object that invoked the block and, as a convenience, the dialog's
364
+ output. So our example from above becomes:
365
+
366
+ menu = Menu.new do |m|
367
+ m.text "What... is your favourite colour?"
368
+ m.choice "blue", "Blue"
369
+ m.choice "yellow", "Yellow"
370
+
371
+ m.no { |dialog,out| puts "auuuuuuuugh." }
372
+ m.yes do |dialog,out|
373
+ if out == "blue"
374
+ puts "Go on. Off you go."
375
+ else
376
+ puts "auuuuuuuugh."
377
+ exit
378
+ end
379
+ end
380
+ end
381
+ menu.show!
382
+
383
+ == Availability and Portability
384
+
385
+ Both the dialog program and the ncurses library are installed by default on a
386
+ number of Linux distributions. A number of similar solutions (like newt &
387
+ whiptail) exist, but, arguably, ncurses and dialog are the most widely
388
+ supported tools. If dialog and/or ncurses are not installed on your system,
389
+ they usually can be using your favourite package manager, or built from
390
+ source.
391
+
392
+ Both ncurses and dialog have successfully been ported to a large number of
393
+ systems, including BSD, MacOS X and Windows. <em>Note, however, that the
394
+ dialog gem has only been tested on Linux so far and will most likely fail on
395
+ non-Unix systems</em>. The reason will probably be some small issue with wrong
396
+ pathnames or similar. Please report a bug (see below) if the dialog gem fails
397
+ to invoke the dialog program on your platform.
398
+
399
+ For more information, check out the homepages of
400
+ ncurses[http://invisible-island.net/ncurses/] and
401
+ dialog[http://invisible-island.net/dialog/], especially the ncurses
402
+ FAQ[http://invisible-island.net/ncurses/ncurses.faq.html]
403
+
404
+ == Known bugs
405
+
406
+ No unit tests.
407
+
408
+ == Support (aka Reporting bugs)
409
+
410
+ The dialog homepage is dialog.rubyforge.org. You can download all releases of
411
+ dialog from there, including the trunk from subversion. Please report bugs or
412
+ requests for enhancement to dialog's rubyforge
413
+ tracker[http://rubyforge.org/tracker/?group_id=2153]. If possible, please
414
+ submit patches. Patches should be generated by "diff -u" or "svn diff" and
415
+ state against which release/svn revision they were made.
@@ -0,0 +1,346 @@
1
+ require 'date'
2
+ require 'util'
3
+
4
+ # Module containing all dialog stuff, to avoid namespace clutter
5
+ module Dialog
6
+
7
+ # Constants for dialog(1) exit statii
8
+ Ok = Yes = 0
9
+ Cancel = No = 1
10
+ Help = 2
11
+ Extra = 3
12
+ Error = -1
13
+
14
+ # Base class collecting common options and handlers for a dialog
15
+ class DialogOptions
16
+
17
+ attr_reader :box_options, :common_options, :handlers
18
+
19
+ # Initializes the collector with default options.
20
+ def initialize(height = 0, width = 0)
21
+ @handlers = {}
22
+ @box_options = ["Er, perhaps you should set some text using the text method!", height, width]
23
+ @common_options = {}
24
+ yield self if block_given?
25
+ end
26
+
27
+ # Sets the label for the ok button and optionally attaches a block as ok handler
28
+ def yes(label="Yes", &block)
29
+ @common_options.store('yes-label', label)
30
+ @handlers[:yes] = block
31
+ end
32
+
33
+ # Sets the label for the ok button and optionally attaches a block as ok handler
34
+ def no(label="No", &block)
35
+ @common_options.store('no-label', label)
36
+ @handlers[:no] = block
37
+ end
38
+
39
+ # Sets the label for the ok button and optionally attaches a block as ok handler
40
+ def ok(label="OK", &block)
41
+ @common_options.store('ok-label', label)
42
+ @handlers[:ok] = block
43
+ end
44
+
45
+ # Sets the label for the cancel button and optionally attaches a block as cancel handler
46
+ def cancel(label="Cancel", &block)
47
+ @common_options.store('cancel-label', label)
48
+ @handlers[:cancel] = block
49
+ end
50
+
51
+ # Sets the label for the extra button and optionally attaches a block as extra handler
52
+ def extra(label="Rename", &block)
53
+ @common_options.store('extra-button', nil)
54
+ @common_options.store('extra-label', label)
55
+ @handlers[:extra] = block
56
+ end
57
+
58
+ # Attaches a block as help handler. No label can be specified, because help is hardwired to F1.
59
+ def help(&block)
60
+ @common_options.store('help-button', nil)
61
+ @handlers[:help] = block
62
+ end
63
+
64
+ # Sets the dialog's width
65
+ def width(w)
66
+ @box_options[2] = w
67
+ end
68
+
69
+ # Sets the dialog's height
70
+ def height(h)
71
+ @box_options[1] = h
72
+ end
73
+
74
+ # Sets the dialog's text
75
+ def text(t)
76
+ @box_options[0] = t
77
+ end
78
+
79
+ # Map unknown method names to common options
80
+ def method_missing(name, *args)
81
+ @common_options.store(name.to_s.gsub('_','-'), args);
82
+ end
83
+
84
+ # Creates a semi-shallow copy of the object
85
+ #
86
+ # The object and its immediate data structures are copied. The instances
87
+ # they refer to, however, are not.
88
+ def dup
89
+ opts = DialogOptions.new
90
+ opts.box_options = @box_options.dup
91
+ opts.handlers = @handlers.dup
92
+ opts.common_options = @common_options.dup
93
+ opts
94
+ end
95
+
96
+ protected
97
+ def box_options=(opts)
98
+ @box_options = opts
99
+ end
100
+
101
+ def common_options=(opts)
102
+ @common_options = opts
103
+ end
104
+
105
+ def handlers=(opts)
106
+ @handlers = opts
107
+ end
108
+ end
109
+
110
+ private
111
+ # Default DialogOptions instance
112
+ DefaultOptions = DialogOptions.new
113
+
114
+ public
115
+ # Configures default options for all dialogs
116
+ #
117
+ # Returns the DialogOptions instance, that is used as the default
118
+ # by all dialog objects (unless explicitly overridden).
119
+ # Supports block-style initialization:
120
+ #
121
+ # Dialog::default_options do |d|
122
+ # d.backtitle "Camelot!"
123
+ # d.no_shadow
124
+ # end
125
+ #
126
+ def default_options
127
+ yield DefaultOptions if block_given?
128
+ DefaultOptions
129
+ end
130
+
131
+
132
+ # Base-class for all dialog "widgets".
133
+ #
134
+ # Contains functionality common to all widgets, like the
135
+ # handling of common options, assembling the command-line
136
+ # and invoking the dialog command.
137
+ #
138
+ # Author: Martin Landers <martin.landers@jambit.com>
139
+ class Base
140
+
141
+ attr_reader :output
142
+
143
+ private
144
+ # The command used to invoke dialog
145
+ Dialog_cmd = (`which dialog` || "/usr/bin/dialog").chomp
146
+
147
+ public
148
+ # Initializes a dialog "widget".
149
+ #
150
+ # Does base initialization common to all widgets.
151
+ def initialize(height = 0, width = 0, options = Dialog::default_options.dup)
152
+ @options = options
153
+ @options.height(height) if height != 0
154
+ @options.width(width) if width != 0
155
+ yield self if block_given?
156
+ end
157
+
158
+ # Predicate, testing if the dialog was left with the YES button
159
+ def yes?
160
+ @exitstatus == Yes
161
+ end
162
+
163
+ # Predicate, testing if the dialog was left with the NO button
164
+ def no?
165
+ @exitstatus == No
166
+ end
167
+
168
+ # Predicate, testing if the dialog was left with the OK button
169
+ def ok?
170
+ @exitstatus == Ok
171
+ end
172
+
173
+ # Predicate, testing if the dialog was left using the Cancel button
174
+ def cancel?
175
+ @exitstatus == Cancel
176
+ end
177
+
178
+ # Predicate, testing if the dialog was left using the Extra button
179
+ def extra?
180
+ @exitstatus == Extra
181
+ end
182
+
183
+ # Predicate, testing if the dialog was left using the Help button
184
+ def help?
185
+ @exitstatus == Help
186
+ end
187
+
188
+ # Gets the fixed-position box options
189
+ #
190
+ # Returns a duplicate, so subclasses can use destructive
191
+ # array operations like <<.
192
+ def box_options
193
+ @options.box_options.dup
194
+ end
195
+
196
+ # Gets the registered handlers
197
+ #
198
+ # Returns a duplicate, so subclasses can use destructive
199
+ # array operations like <<.
200
+ def handlers
201
+ @options.handlers.dup
202
+ end
203
+
204
+ # Gets the common options for the dialog.
205
+ #
206
+ # The options are stored as a hash mapping option names to value arrays
207
+ # or +nil+ if no value is associated with the option.
208
+ #
209
+ # Returns a duplicate, so subclasses can use destructive
210
+ # array operations like <<.
211
+ def common_options
212
+ @options.common_options.dup
213
+ end
214
+
215
+ # Turns the name of the current class into a box option
216
+ #
217
+ # For example, class FSelect becomes --fselect
218
+ def box_type
219
+ "--#{self.class.name.split('::').last.downcase}"
220
+ end
221
+
222
+ # Delegate unknown method names to the options instance
223
+ def method_missing(name, *args, &block)
224
+ @options.send(name, *args, &block)
225
+ end
226
+
227
+ # Asynchronously displays the dialog
228
+ #
229
+ # That is, forks off the dialog program in a seperate
230
+ # process and returns immediately, without waiting for
231
+ # user input. This is useful for widgets like Gauge,
232
+ # that require concurrent processing and may receive
233
+ # updates from the application. Note, that you must
234
+ # use the wait method to wait until the user has closed
235
+ # the dialog, before you can use any of the status
236
+ # methods of the dialog like ok?.
237
+ def show
238
+ invoke
239
+ end
240
+
241
+ # Synchronously displays the dialog
242
+ #
243
+ # That is, invokes the dialog program and blocks until
244
+ # it has returned. The method returns self, just like
245
+ # the wait method (see example there). Use this method
246
+ # to display simple input dialogs, that don't require
247
+ # concurrent processing.
248
+ def show!
249
+ invoke
250
+ wait
251
+ end
252
+
253
+ # Waits for the user to close/cancel the dialog.
254
+ #
255
+ # That is, waits for the dialog program to return.
256
+ # Returns self. Use the ok?, cancel?, etc. predicates
257
+ # to determine exit status, like this:
258
+ #
259
+ # if dlg.wait.ok?
260
+ # ...
261
+ # else
262
+ # ...
263
+ # end
264
+ #
265
+ # If forking or waiting for the process fails, a SystemCallError
266
+ # is raised. If dialog exits with an error message, a DialogError
267
+ # is raised.
268
+ def wait
269
+ raise DialogError, "No dialog to wait for" unless @pid
270
+
271
+ pid, status = Process.wait2(@pid)
272
+ @exitstatus = status.exitstatus
273
+ @output = @stderr.read
274
+
275
+ if @exitstatus == 255
276
+ # Raise an exception if dialog printed an error message
277
+ # If not, the user just exited the dialog using ESC
278
+ raise DialogError.new(commandline_arguments), @output unless @output.empty?
279
+ end
280
+
281
+ case @exitstatus
282
+ when Ok then
283
+ handlers[:ok].call(self, @output) if handlers[:ok]
284
+ handlers[:yes].call(self, @output) if handlers[:yes]
285
+ when Cancel then
286
+ handlers[:cancel].call(self, @output) if handlers[:cancel]
287
+ handlers[:no].call(self, @output) if handlers[:no]
288
+ when Extra then
289
+ handlers[:extra].call(self, @output) if handlers[:extra]
290
+ when Help then
291
+ handlers[:help].call(self, @output) if handlers[:help]
292
+ end
293
+ self
294
+ ensure
295
+ @pid = @stdin = @stderr = nil
296
+ end
297
+
298
+ # Gets the commandline arguments that show would pass to dialog.
299
+ #
300
+ # Use this method if you only want to use the dialog classes as
301
+ # fancy string builders and need to invoke the dialog program
302
+ # yourself. Note, that the arguments will be returned as an array
303
+ # to avoid quoting problems, and will not include the actual
304
+ # command used to invoke dialog program, only the commandline options.
305
+ def commandline_arguments
306
+ collect_options
307
+ end
308
+
309
+ protected
310
+
311
+ # Asynchronously invokes the dialog command
312
+ #
313
+ # Initializes @pid, @stdin and @stderr. Resets @exitstatus.
314
+ def invoke
315
+ @exitstatus = nil
316
+ @pid, @stdin, stdout, @stderr = Util.popen3(Dialog_cmd, collect_options, :stdout => STDOUT)
317
+ end
318
+
319
+ # Collects all the options into an options string suitable for dialog(1)
320
+ def collect_options
321
+ opts = []
322
+ common_options.each do |option,value|
323
+ opts << "--#{option}"
324
+ unless value.nil? || value.empty?
325
+ value = [value].flatten
326
+ opts += value
327
+ end
328
+ end
329
+ opts << box_type
330
+ opts += box_options
331
+ opts.map {|o| o.to_s }
332
+ end
333
+ end
334
+
335
+ # Used to signal exceptions when invoking dialog
336
+ class DialogError < RuntimeError
337
+ def initialize(args)
338
+ @args = args
339
+ end
340
+
341
+ def to_s
342
+ "dialog has reported an error\n#{super}\narguments: #{@args.inspect}\n"
343
+ end
344
+ end
345
+
346
+ end