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