dialog.py revision 244850
1# $Id: dialog.py,v 1.3 2004/09/21 00:52:15 tom Exp $
2# Module: dialog.py
3# Copyright (c) 2000 Robb Shecter <robb@acm.org>
4# All rights reserved.
5# This source is covered by the GNU GPL.
6#
7# This module is a Python wrapper around the Linux "dialog" utility
8# by Savio Lam and Stuart Herbert.  My goals were to make dialog as
9# easy to use from Python as possible.  The demo code at the end of
10# the module is a good example of how to use it.  To run the demo,
11# execute:
12#
13#                       python dialog.py
14#
15# This module has one class in it, "Dialog".  An application typically
16# creates an instance of it, and possibly sets the background title option.
17# Then, methods can be called on it for interacting with the user.
18#
19# I wrote this because I want to use my 486-33 laptop as my main
20# development computer (!), and I wanted a way to nicely interact with the
21# user in console mode.  There are apparently other modules out there
22# with similar functionality, but they require the Python curses library.
23# Writing this module from scratch was easier than figuring out how to
24# recompile Python with curses enabled. :)
25#
26# One interesting feature is that the menu and selection windows allow
27# *any* objects to be displayed and selected, not just strings.
28#
29# TO DO:
30#   Add code so that the input buffer is flushed before a dialog box is
31#     shown.  This would make the UI more predictable for users.  This
32#     feature could be turned on and off through an instance method.
33#   Drop using temporary files when interacting with 'dialog'
34#     (it's possible -- I've already tried :-).
35#   Try detecting the terminal window size in order to make reasonable
36#     height and width defaults.  Hmmm - should also then check for
37#     terminal resizing...
38#   Put into a package name to make more reusable - reduce the possibility
39#     of name collisions.
40#
41# NOTES:
42#         there is a bug in (at least) Linux-Mandrake 7.0 Russian Edition
43#         running on AMD K6-2 3D that causes core dump when 'dialog'
44#         is running with --gauge option;
45#         in this case you'll have to recompile 'dialog' program.
46#
47# Modifications:
48# Jul 2000, Sultanbek Tezadov (http://sultan.da.ru)
49#    Added:
50#       - 'gauge' widget *)
51#       - 'title' option to some widgets
52#       - 'checked' option to checklist dialog; clicking "Cancel" is now
53#           recognizable
54#       - 'selected' option to radiolist dialog; clicking "Cancel" is now
55#           recognizable
56#       - some other cosmetic changes and improvements
57#
58
59import os
60from tempfile import mktemp
61from string import split
62from time import sleep
63
64#
65# Path of the dialog executable
66#
67DIALOG = os.getenv("DIALOG");
68if DIALOG is None:
69	DIALOG="../dialog";
70
71class Dialog:
72    def __init__(self):
73	self.__bgTitle = ''               # Default is no background title
74
75
76    def setBackgroundTitle(self, text):
77	self.__bgTitle = '--backtitle "%s"' % text
78
79
80    def __perform(self, cmd):
81	"""Do the actual work of invoking dialog and getting the output."""
82	fName = mktemp()
83	rv = os.system('%s %s %s 2> %s' % (DIALOG, self.__bgTitle, cmd, fName))
84	f = open(fName)
85	output = f.readlines()
86	f.close()
87	os.unlink(fName)
88	return (rv, output)
89
90
91    def __perform_no_options(self, cmd):
92	"""Call dialog w/out passing any more options. Needed by --clear."""
93	return os.system(DIALOG + ' ' + cmd)
94
95
96    def __handleTitle(self, title):
97	if len(title) == 0:
98	    return ''
99	else:
100	    return '--title "%s" ' % title
101
102
103    def yesno(self, text, height=10, width=30, title=''):
104	"""
105	Put a Yes/No question to the user.
106	Uses the dialog --yesno option.
107	Returns a 1 or a 0.
108	"""
109	(code, output) = self.__perform(self.__handleTitle(title) +\
110	    '--yesno "%s" %d %d' % (text, height, width))
111	return code == 0
112
113
114    def msgbox(self, text, height=10, width=30, title=''):
115	"""
116	Pop up a message to the user which has to be clicked
117	away with "ok".
118	"""
119	self.__perform(self.__handleTitle(title) +\
120	    '--msgbox "%s" %d %d' % (text, height, width))
121
122
123    def infobox(self, text, height=10, width=30):
124	"""Make a message to the user, and return immediately."""
125	self.__perform('--infobox "%s" %d %d' % (text, height, width))
126
127
128    def inputbox(self, text, height=10, width=30, init='', title=''):
129	"""
130	Request a line of input from the user.
131	Returns the user's input or None if cancel was chosen.
132	"""
133	(c, o) = self.__perform(self.__handleTitle(title) +\
134	    '--inputbox "%s" %d %d "%s"' % (text, height, width, init))
135	try:
136	    return o[0]
137	except IndexError:
138	    if c == 0:  # empty string entered
139		return ''
140	    else:  # canceled
141		return None
142
143
144    def textbox(self, filename, height=20, width=60, title=None):
145	"""Display a file in a scrolling text box."""
146	if title is None:
147	    title = filename
148	self.__perform(self.__handleTitle(title) +\
149	    ' --textbox "%s" %d %d' % (filename, height, width))
150
151
152    def menu(self, text, height=15, width=54, list=[]):
153	"""
154	Display a menu of options to the user.  This method simplifies the
155	--menu option of dialog, which allows for complex arguments.  This
156	method receives a simple list of objects, and each one is assigned
157	a choice number.
158	The selected object is returned, or None if the dialog was canceled.
159	"""
160	menuheight = height - 8
161	pairs = map(lambda i, item: (i + 1, item), range(len(list)), list)
162	choices = reduce(lambda res, pair: res + '%d "%s" ' % pair, pairs, '')
163	(code, output) = self.__perform('--menu "%s" %d %d %d %s' %\
164	    (text, height, width, menuheight, choices))
165	try:
166	    return list[int(output[0]) - 1]
167	except IndexError:
168	    return None
169
170
171    def checklist(self, text, height=15, width=54, list=[], checked=None):
172	"""
173	Returns a list of the selected objects.
174	Returns an empty list if nothing was selected.
175	Returns None if the window was canceled.
176	checked -- a list of boolean (0/1) values; len(checked) must equal
177	    len(list).
178	"""
179	if checked is None:
180	    checked = [0]*len(list)
181	menuheight = height - 8
182	triples = map(
183	    lambda i, item, onoff, fs=('off', 'on'): (i + 1, item, fs[onoff]),
184	    range(len(list)), list, checked)
185	choices = reduce(lambda res, triple: res + '%d "%s" %s ' % triple,
186	    triples, '')
187	(c, o) = self.__perform('--checklist "%s" %d %d %d %s' %\
188	    (text, height, width, menuheight, choices))
189	try:
190	    output = o[0]
191	    indexList  = map(lambda x: int(x[1:-1]), split(output))
192	    objectList = filter(lambda item, list=list, indexList=indexList:
193		    list.index(item) + 1 in indexList,
194		list)
195	    return objectList
196	except IndexError:
197	    if c == 0:                        # Nothing was selected
198		return []
199	    return None  # Was canceled
200
201
202    def radiolist(self, text, height=15, width=54, list=[], selected=0):
203	"""
204	Return the selected object.
205	Returns empty string if no choice was selected.
206	Returns None if window was canceled.
207	selected -- the selected item (must be between 1 and len(list)
208	    or 0, meaning no selection).
209	"""
210	menuheight = height - 8
211	triples = map(lambda i, item: (i + 1, item, 'off'),
212	    range(len(list)), list)
213	if selected:
214	    i, item, tmp = triples[selected - 1]
215	    triples[selected - 1] = (i, item, 'on')
216	choices = reduce(lambda res, triple: res + '%d "%s" %s ' % triple,
217	    triples, '')
218	(c, o) = self.__perform('--radiolist "%s" %d %d %d %s' %\
219	    (text, height, width, menuheight, choices))
220	try:
221	    return list[int(o[0]) - 1]
222	except IndexError:
223	    if c == 0:
224		return ''
225	    return None
226
227
228    def clear(self):
229	"""
230	Clear the screen. Equivalent to the dialog --clear option.
231	"""
232	self.__perform_no_options('--clear')
233
234
235    def scrollbox(self, text, height=20, width=60, title=''):
236	"""
237	This is a bonus method.  The dialog package only has a function to
238	display a file in a scrolling text field.  This method allows any
239	string to be displayed by first saving it in a temp file, and calling
240	--textbox.
241	"""
242	fName = mktemp()
243	f = open(fName, 'w')
244	f.write(text)
245	f.close()
246	self.__perform(self.__handleTitle(title) +\
247	    '--textbox "%s" %d %d' % (fName, height, width))
248	os.unlink(fName)
249
250
251    def gauge_start(self, perc=0, text='', height=8, width=54, title=''):
252	"""
253	Display gauge output window.
254	Gauge normal usage (assuming that there is an instace of 'Dialog'
255	class named 'd'):
256	    d.gauge_start()
257	    # do something
258	    d.gauge_iterate(10)  # passed throgh 10%
259	    # ...
260	    d.gauge_iterate(100, 'any text here')  # work is done
261	    d.stop_gauge()  # clean-up actions
262	"""
263	cmd = self.__handleTitle(title) +\
264	    '--gauge "%s" %d %d %d' % (text, height, width, perc)
265	cmd = '%s %s %s 2> /dev/null' % (DIALOG, self.__bgTitle, cmd)
266	self.pipe = os.popen(cmd, 'w')
267    #/gauge_start()
268
269
270    def gauge_iterate(self, perc, text=''):
271	"""
272	Update percentage point value.
273
274	See gauge_start() function above for the usage.
275	"""
276	if text:
277	    text = 'XXX\n%d\n%s\nXXX\n' % (perc, text)
278	else:
279	    text = '%d\n' % perc
280	self.pipe.write(text)
281	self.pipe.flush()
282    #/gauge_iterate()
283
284
285    def gauge_stop(self):
286	"""
287	Finish previously started gauge.
288
289	See gauge_start() function above for the usage.
290	"""
291	self.pipe.close()
292    #/gauge_stop()
293
294
295
296#
297# DEMO APPLICATION
298#
299if __name__ == '__main__':
300    """
301    This demo tests all the features of the class.
302    """
303    d = Dialog()
304    d.setBackgroundTitle('dialog.py demo')
305
306    d.infobox(
307	"One moment... Just wasting some time here to test the infobox...")
308    sleep(3)
309
310    if d.yesno("Do you like this demo?"):
311	d.msgbox("Excellent!  Here's the source code:")
312    else:
313	d.msgbox("Send your complaints to /dev/null")
314
315    d.textbox("dialog.py")
316
317    name = d.inputbox("What's your name?", init="Snow White")
318    fday = d.menu("What's your favorite day of the week?",
319	list=["Monday", "Tuesday", "Wednesday", "Thursday",
320	    "Friday (The best day of all)", "Saturday", "Sunday"])
321    food = d.checklist("What sandwich toppings do you like?",
322	list=["Catsup", "Mustard", "Pesto", "Mayonaise", "Horse radish",
323	    "Sun-dried tomatoes"], checked=[0,0,0,1,1,1])
324    sand = d.radiolist("What's your favorite kind of sandwich?",
325	list=["Hamburger", "Hotdog", "Burrito", "Doener", "Falafel",
326	    "Bagel", "Big Mac", "Whopper", "Quarter Pounder",
327	    "Peanut Butter and Jelly", "Grilled cheese"], selected=4)
328
329    # Prepare the message for the final window
330    bigMessage = "Here are some vital statistics about you:\n\nName: " + name +\
331        "\nFavorite day of the week: " + fday +\
332	"\nFavorite sandwich toppings:\n"
333    for topping in food:
334	bigMessage = bigMessage + "    " + topping + "\n"
335    bigMessage = bigMessage + "Favorite sandwich: " + str(sand)
336
337    d.scrollbox(bigMessage)
338
339    #<>#  Gauge Demo
340    d.gauge_start(0, 'percentage: 0', title='Gauge Demo')
341    for i in range(1, 101):
342	if i < 50:
343	    msg = 'percentage: %d' % i
344	elif i == 50:
345	    msg = 'Over 50%'
346	else:
347	    msg = ''
348	d.gauge_iterate(i, msg)
349	sleep(0.1)
350    d.gauge_stop()
351    #<>#
352
353    d.clear()
354