dialog 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +415 -0
- data/lib/base.rb +346 -0
- data/lib/calendar.rb +31 -0
- data/lib/checklist.rb +33 -0
- data/lib/dialog.rb +17 -0
- data/lib/form.rb +44 -0
- data/lib/fselect.rb +32 -0
- data/lib/gauge.rb +46 -0
- data/lib/infobox.rb +21 -0
- data/lib/inputbox.rb +26 -0
- data/lib/inputmenu.rb +22 -0
- data/lib/menu.rb +46 -0
- data/lib/msgbox.rb +19 -0
- data/lib/passwordbox.rb +20 -0
- data/lib/radiolist.rb +28 -0
- data/lib/tailbox.rb +20 -0
- data/lib/tailboxbg.rb +21 -0
- data/lib/textbox.rb +29 -0
- data/lib/timebox.rb +30 -0
- data/lib/util.rb +74 -0
- data/lib/yesno.rb +19 -0
- metadata +64 -0
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.
|
data/lib/base.rb
ADDED
@@ -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
|