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 +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
|