dialog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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