1251843Sbapt#!/usr/bin/python
2251843Sbapt# $Id: dialog.py,v 1.4 2012/06/29 09:33:18 tom Exp $
3217309Snwhitehorn# Module: dialog.py
4217309Snwhitehorn# Copyright (c) 2000 Robb Shecter <robb@acm.org>
5217309Snwhitehorn# All rights reserved.
6217309Snwhitehorn# This source is covered by the GNU GPL.
7217309Snwhitehorn#
8217309Snwhitehorn# This module is a Python wrapper around the Linux "dialog" utility
9217309Snwhitehorn# by Savio Lam and Stuart Herbert.  My goals were to make dialog as
10217309Snwhitehorn# easy to use from Python as possible.  The demo code at the end of
11217309Snwhitehorn# the module is a good example of how to use it.  To run the demo,
12217309Snwhitehorn# execute:
13217309Snwhitehorn#
14217309Snwhitehorn#                       python dialog.py
15217309Snwhitehorn#
16217309Snwhitehorn# This module has one class in it, "Dialog".  An application typically
17217309Snwhitehorn# creates an instance of it, and possibly sets the background title option.
18217309Snwhitehorn# Then, methods can be called on it for interacting with the user.
19217309Snwhitehorn#
20217309Snwhitehorn# I wrote this because I want to use my 486-33 laptop as my main
21217309Snwhitehorn# development computer (!), and I wanted a way to nicely interact with the
22217309Snwhitehorn# user in console mode.  There are apparently other modules out there
23217309Snwhitehorn# with similar functionality, but they require the Python curses library.
24217309Snwhitehorn# Writing this module from scratch was easier than figuring out how to
25217309Snwhitehorn# recompile Python with curses enabled. :)
26217309Snwhitehorn#
27217309Snwhitehorn# One interesting feature is that the menu and selection windows allow
28217309Snwhitehorn# *any* objects to be displayed and selected, not just strings.
29217309Snwhitehorn#
30217309Snwhitehorn# TO DO:
31217309Snwhitehorn#   Add code so that the input buffer is flushed before a dialog box is
32217309Snwhitehorn#     shown.  This would make the UI more predictable for users.  This
33217309Snwhitehorn#     feature could be turned on and off through an instance method.
34217309Snwhitehorn#   Drop using temporary files when interacting with 'dialog'
35217309Snwhitehorn#     (it's possible -- I've already tried :-).
36217309Snwhitehorn#   Try detecting the terminal window size in order to make reasonable
37217309Snwhitehorn#     height and width defaults.  Hmmm - should also then check for
38217309Snwhitehorn#     terminal resizing...
39217309Snwhitehorn#   Put into a package name to make more reusable - reduce the possibility
40217309Snwhitehorn#     of name collisions.
41217309Snwhitehorn#
42217309Snwhitehorn# NOTES:
43217309Snwhitehorn#         there is a bug in (at least) Linux-Mandrake 7.0 Russian Edition
44217309Snwhitehorn#         running on AMD K6-2 3D that causes core dump when 'dialog'
45217309Snwhitehorn#         is running with --gauge option;
46217309Snwhitehorn#         in this case you'll have to recompile 'dialog' program.
47217309Snwhitehorn#
48217309Snwhitehorn# Modifications:
49217309Snwhitehorn# Jul 2000, Sultanbek Tezadov (http://sultan.da.ru)
50217309Snwhitehorn#    Added:
51217309Snwhitehorn#       - 'gauge' widget *)
52217309Snwhitehorn#       - 'title' option to some widgets
53217309Snwhitehorn#       - 'checked' option to checklist dialog; clicking "Cancel" is now
54217309Snwhitehorn#           recognizable
55217309Snwhitehorn#       - 'selected' option to radiolist dialog; clicking "Cancel" is now
56217309Snwhitehorn#           recognizable
57217309Snwhitehorn#       - some other cosmetic changes and improvements
58217309Snwhitehorn#
59217309Snwhitehorn
60217309Snwhitehornimport os
61217309Snwhitehornfrom tempfile import mktemp
62217309Snwhitehornfrom string import split
63217309Snwhitehornfrom time import sleep
64217309Snwhitehorn
65217309Snwhitehorn#
66217309Snwhitehorn# Path of the dialog executable
67217309Snwhitehorn#
68217309SnwhitehornDIALOG = os.getenv("DIALOG");
69217309Snwhitehornif DIALOG is None:
70217309Snwhitehorn	DIALOG="../dialog";
71217309Snwhitehorn
72217309Snwhitehornclass Dialog:
73217309Snwhitehorn    def __init__(self):
74217309Snwhitehorn	self.__bgTitle = ''               # Default is no background title
75217309Snwhitehorn
76217309Snwhitehorn
77217309Snwhitehorn    def setBackgroundTitle(self, text):
78217309Snwhitehorn	self.__bgTitle = '--backtitle "%s"' % text
79217309Snwhitehorn
80217309Snwhitehorn
81217309Snwhitehorn    def __perform(self, cmd):
82217309Snwhitehorn	"""Do the actual work of invoking dialog and getting the output."""
83217309Snwhitehorn	fName = mktemp()
84217309Snwhitehorn	rv = os.system('%s %s %s 2> %s' % (DIALOG, self.__bgTitle, cmd, fName))
85217309Snwhitehorn	f = open(fName)
86217309Snwhitehorn	output = f.readlines()
87217309Snwhitehorn	f.close()
88217309Snwhitehorn	os.unlink(fName)
89217309Snwhitehorn	return (rv, output)
90217309Snwhitehorn
91217309Snwhitehorn
92217309Snwhitehorn    def __perform_no_options(self, cmd):
93217309Snwhitehorn	"""Call dialog w/out passing any more options. Needed by --clear."""
94217309Snwhitehorn	return os.system(DIALOG + ' ' + cmd)
95217309Snwhitehorn
96217309Snwhitehorn
97217309Snwhitehorn    def __handleTitle(self, title):
98217309Snwhitehorn	if len(title) == 0:
99217309Snwhitehorn	    return ''
100217309Snwhitehorn	else:
101217309Snwhitehorn	    return '--title "%s" ' % title
102217309Snwhitehorn
103217309Snwhitehorn
104217309Snwhitehorn    def yesno(self, text, height=10, width=30, title=''):
105217309Snwhitehorn	"""
106217309Snwhitehorn	Put a Yes/No question to the user.
107217309Snwhitehorn	Uses the dialog --yesno option.
108217309Snwhitehorn	Returns a 1 or a 0.
109217309Snwhitehorn	"""
110217309Snwhitehorn	(code, output) = self.__perform(self.__handleTitle(title) +\
111217309Snwhitehorn	    '--yesno "%s" %d %d' % (text, height, width))
112217309Snwhitehorn	return code == 0
113217309Snwhitehorn
114217309Snwhitehorn
115217309Snwhitehorn    def msgbox(self, text, height=10, width=30, title=''):
116217309Snwhitehorn	"""
117217309Snwhitehorn	Pop up a message to the user which has to be clicked
118217309Snwhitehorn	away with "ok".
119217309Snwhitehorn	"""
120217309Snwhitehorn	self.__perform(self.__handleTitle(title) +\
121217309Snwhitehorn	    '--msgbox "%s" %d %d' % (text, height, width))
122217309Snwhitehorn
123217309Snwhitehorn
124217309Snwhitehorn    def infobox(self, text, height=10, width=30):
125217309Snwhitehorn	"""Make a message to the user, and return immediately."""
126217309Snwhitehorn	self.__perform('--infobox "%s" %d %d' % (text, height, width))
127217309Snwhitehorn
128217309Snwhitehorn
129217309Snwhitehorn    def inputbox(self, text, height=10, width=30, init='', title=''):
130217309Snwhitehorn	"""
131217309Snwhitehorn	Request a line of input from the user.
132217309Snwhitehorn	Returns the user's input or None if cancel was chosen.
133217309Snwhitehorn	"""
134217309Snwhitehorn	(c, o) = self.__perform(self.__handleTitle(title) +\
135217309Snwhitehorn	    '--inputbox "%s" %d %d "%s"' % (text, height, width, init))
136217309Snwhitehorn	try:
137217309Snwhitehorn	    return o[0]
138217309Snwhitehorn	except IndexError:
139217309Snwhitehorn	    if c == 0:  # empty string entered
140217309Snwhitehorn		return ''
141217309Snwhitehorn	    else:  # canceled
142217309Snwhitehorn		return None
143217309Snwhitehorn
144217309Snwhitehorn
145217309Snwhitehorn    def textbox(self, filename, height=20, width=60, title=None):
146217309Snwhitehorn	"""Display a file in a scrolling text box."""
147217309Snwhitehorn	if title is None:
148217309Snwhitehorn	    title = filename
149217309Snwhitehorn	self.__perform(self.__handleTitle(title) +\
150217309Snwhitehorn	    ' --textbox "%s" %d %d' % (filename, height, width))
151217309Snwhitehorn
152217309Snwhitehorn
153217309Snwhitehorn    def menu(self, text, height=15, width=54, list=[]):
154217309Snwhitehorn	"""
155217309Snwhitehorn	Display a menu of options to the user.  This method simplifies the
156217309Snwhitehorn	--menu option of dialog, which allows for complex arguments.  This
157217309Snwhitehorn	method receives a simple list of objects, and each one is assigned
158217309Snwhitehorn	a choice number.
159217309Snwhitehorn	The selected object is returned, or None if the dialog was canceled.
160217309Snwhitehorn	"""
161217309Snwhitehorn	menuheight = height - 8
162217309Snwhitehorn	pairs = map(lambda i, item: (i + 1, item), range(len(list)), list)
163217309Snwhitehorn	choices = reduce(lambda res, pair: res + '%d "%s" ' % pair, pairs, '')
164217309Snwhitehorn	(code, output) = self.__perform('--menu "%s" %d %d %d %s' %\
165217309Snwhitehorn	    (text, height, width, menuheight, choices))
166217309Snwhitehorn	try:
167217309Snwhitehorn	    return list[int(output[0]) - 1]
168217309Snwhitehorn	except IndexError:
169217309Snwhitehorn	    return None
170217309Snwhitehorn
171217309Snwhitehorn
172217309Snwhitehorn    def checklist(self, text, height=15, width=54, list=[], checked=None):
173217309Snwhitehorn	"""
174217309Snwhitehorn	Returns a list of the selected objects.
175217309Snwhitehorn	Returns an empty list if nothing was selected.
176217309Snwhitehorn	Returns None if the window was canceled.
177217309Snwhitehorn	checked -- a list of boolean (0/1) values; len(checked) must equal
178217309Snwhitehorn	    len(list).
179217309Snwhitehorn	"""
180217309Snwhitehorn	if checked is None:
181217309Snwhitehorn	    checked = [0]*len(list)
182217309Snwhitehorn	menuheight = height - 8
183217309Snwhitehorn	triples = map(
184217309Snwhitehorn	    lambda i, item, onoff, fs=('off', 'on'): (i + 1, item, fs[onoff]),
185217309Snwhitehorn	    range(len(list)), list, checked)
186217309Snwhitehorn	choices = reduce(lambda res, triple: res + '%d "%s" %s ' % triple,
187217309Snwhitehorn	    triples, '')
188217309Snwhitehorn	(c, o) = self.__perform('--checklist "%s" %d %d %d %s' %\
189217309Snwhitehorn	    (text, height, width, menuheight, choices))
190217309Snwhitehorn	try:
191217309Snwhitehorn	    output = o[0]
192217309Snwhitehorn	    indexList  = map(lambda x: int(x[1:-1]), split(output))
193217309Snwhitehorn	    objectList = filter(lambda item, list=list, indexList=indexList:
194217309Snwhitehorn		    list.index(item) + 1 in indexList,
195217309Snwhitehorn		list)
196217309Snwhitehorn	    return objectList
197217309Snwhitehorn	except IndexError:
198217309Snwhitehorn	    if c == 0:                        # Nothing was selected
199217309Snwhitehorn		return []
200217309Snwhitehorn	    return None  # Was canceled
201217309Snwhitehorn
202217309Snwhitehorn
203217309Snwhitehorn    def radiolist(self, text, height=15, width=54, list=[], selected=0):
204217309Snwhitehorn	"""
205217309Snwhitehorn	Return the selected object.
206217309Snwhitehorn	Returns empty string if no choice was selected.
207217309Snwhitehorn	Returns None if window was canceled.
208217309Snwhitehorn	selected -- the selected item (must be between 1 and len(list)
209217309Snwhitehorn	    or 0, meaning no selection).
210217309Snwhitehorn	"""
211217309Snwhitehorn	menuheight = height - 8
212217309Snwhitehorn	triples = map(lambda i, item: (i + 1, item, 'off'),
213217309Snwhitehorn	    range(len(list)), list)
214217309Snwhitehorn	if selected:
215217309Snwhitehorn	    i, item, tmp = triples[selected - 1]
216217309Snwhitehorn	    triples[selected - 1] = (i, item, 'on')
217217309Snwhitehorn	choices = reduce(lambda res, triple: res + '%d "%s" %s ' % triple,
218217309Snwhitehorn	    triples, '')
219217309Snwhitehorn	(c, o) = self.__perform('--radiolist "%s" %d %d %d %s' %\
220217309Snwhitehorn	    (text, height, width, menuheight, choices))
221217309Snwhitehorn	try:
222217309Snwhitehorn	    return list[int(o[0]) - 1]
223217309Snwhitehorn	except IndexError:
224217309Snwhitehorn	    if c == 0:
225217309Snwhitehorn		return ''
226217309Snwhitehorn	    return None
227217309Snwhitehorn
228217309Snwhitehorn
229217309Snwhitehorn    def clear(self):
230217309Snwhitehorn	"""
231217309Snwhitehorn	Clear the screen. Equivalent to the dialog --clear option.
232217309Snwhitehorn	"""
233217309Snwhitehorn	self.__perform_no_options('--clear')
234217309Snwhitehorn
235217309Snwhitehorn
236217309Snwhitehorn    def scrollbox(self, text, height=20, width=60, title=''):
237217309Snwhitehorn	"""
238217309Snwhitehorn	This is a bonus method.  The dialog package only has a function to
239217309Snwhitehorn	display a file in a scrolling text field.  This method allows any
240217309Snwhitehorn	string to be displayed by first saving it in a temp file, and calling
241217309Snwhitehorn	--textbox.
242217309Snwhitehorn	"""
243217309Snwhitehorn	fName = mktemp()
244217309Snwhitehorn	f = open(fName, 'w')
245217309Snwhitehorn	f.write(text)
246217309Snwhitehorn	f.close()
247217309Snwhitehorn	self.__perform(self.__handleTitle(title) +\
248217309Snwhitehorn	    '--textbox "%s" %d %d' % (fName, height, width))
249217309Snwhitehorn	os.unlink(fName)
250217309Snwhitehorn
251217309Snwhitehorn
252217309Snwhitehorn    def gauge_start(self, perc=0, text='', height=8, width=54, title=''):
253217309Snwhitehorn	"""
254217309Snwhitehorn	Display gauge output window.
255217309Snwhitehorn	Gauge normal usage (assuming that there is an instace of 'Dialog'
256217309Snwhitehorn	class named 'd'):
257217309Snwhitehorn	    d.gauge_start()
258217309Snwhitehorn	    # do something
259217309Snwhitehorn	    d.gauge_iterate(10)  # passed throgh 10%
260217309Snwhitehorn	    # ...
261217309Snwhitehorn	    d.gauge_iterate(100, 'any text here')  # work is done
262217309Snwhitehorn	    d.stop_gauge()  # clean-up actions
263217309Snwhitehorn	"""
264217309Snwhitehorn	cmd = self.__handleTitle(title) +\
265217309Snwhitehorn	    '--gauge "%s" %d %d %d' % (text, height, width, perc)
266217309Snwhitehorn	cmd = '%s %s %s 2> /dev/null' % (DIALOG, self.__bgTitle, cmd)
267217309Snwhitehorn	self.pipe = os.popen(cmd, 'w')
268217309Snwhitehorn    #/gauge_start()
269217309Snwhitehorn
270217309Snwhitehorn
271217309Snwhitehorn    def gauge_iterate(self, perc, text=''):
272217309Snwhitehorn	"""
273217309Snwhitehorn	Update percentage point value.
274217309Snwhitehorn
275217309Snwhitehorn	See gauge_start() function above for the usage.
276217309Snwhitehorn	"""
277217309Snwhitehorn	if text:
278217309Snwhitehorn	    text = 'XXX\n%d\n%s\nXXX\n' % (perc, text)
279217309Snwhitehorn	else:
280217309Snwhitehorn	    text = '%d\n' % perc
281217309Snwhitehorn	self.pipe.write(text)
282217309Snwhitehorn	self.pipe.flush()
283217309Snwhitehorn    #/gauge_iterate()
284217309Snwhitehorn
285217309Snwhitehorn
286217309Snwhitehorn    def gauge_stop(self):
287217309Snwhitehorn	"""
288217309Snwhitehorn	Finish previously started gauge.
289217309Snwhitehorn
290217309Snwhitehorn	See gauge_start() function above for the usage.
291217309Snwhitehorn	"""
292217309Snwhitehorn	self.pipe.close()
293217309Snwhitehorn    #/gauge_stop()
294217309Snwhitehorn
295217309Snwhitehorn
296217309Snwhitehorn
297217309Snwhitehorn#
298217309Snwhitehorn# DEMO APPLICATION
299217309Snwhitehorn#
300217309Snwhitehornif __name__ == '__main__':
301217309Snwhitehorn    """
302217309Snwhitehorn    This demo tests all the features of the class.
303217309Snwhitehorn    """
304217309Snwhitehorn    d = Dialog()
305217309Snwhitehorn    d.setBackgroundTitle('dialog.py demo')
306217309Snwhitehorn
307217309Snwhitehorn    d.infobox(
308217309Snwhitehorn	"One moment... Just wasting some time here to test the infobox...")
309217309Snwhitehorn    sleep(3)
310217309Snwhitehorn
311217309Snwhitehorn    if d.yesno("Do you like this demo?"):
312217309Snwhitehorn	d.msgbox("Excellent!  Here's the source code:")
313217309Snwhitehorn    else:
314217309Snwhitehorn	d.msgbox("Send your complaints to /dev/null")
315217309Snwhitehorn
316217309Snwhitehorn    d.textbox("dialog.py")
317217309Snwhitehorn
318217309Snwhitehorn    name = d.inputbox("What's your name?", init="Snow White")
319217309Snwhitehorn    fday = d.menu("What's your favorite day of the week?",
320217309Snwhitehorn	list=["Monday", "Tuesday", "Wednesday", "Thursday",
321217309Snwhitehorn	    "Friday (The best day of all)", "Saturday", "Sunday"])
322217309Snwhitehorn    food = d.checklist("What sandwich toppings do you like?",
323217309Snwhitehorn	list=["Catsup", "Mustard", "Pesto", "Mayonaise", "Horse radish",
324217309Snwhitehorn	    "Sun-dried tomatoes"], checked=[0,0,0,1,1,1])
325217309Snwhitehorn    sand = d.radiolist("What's your favorite kind of sandwich?",
326217309Snwhitehorn	list=["Hamburger", "Hotdog", "Burrito", "Doener", "Falafel",
327217309Snwhitehorn	    "Bagel", "Big Mac", "Whopper", "Quarter Pounder",
328217309Snwhitehorn	    "Peanut Butter and Jelly", "Grilled cheese"], selected=4)
329217309Snwhitehorn
330217309Snwhitehorn    # Prepare the message for the final window
331217309Snwhitehorn    bigMessage = "Here are some vital statistics about you:\n\nName: " + name +\
332217309Snwhitehorn        "\nFavorite day of the week: " + fday +\
333217309Snwhitehorn	"\nFavorite sandwich toppings:\n"
334217309Snwhitehorn    for topping in food:
335217309Snwhitehorn	bigMessage = bigMessage + "    " + topping + "\n"
336217309Snwhitehorn    bigMessage = bigMessage + "Favorite sandwich: " + str(sand)
337217309Snwhitehorn
338217309Snwhitehorn    d.scrollbox(bigMessage)
339217309Snwhitehorn
340217309Snwhitehorn    #<>#  Gauge Demo
341217309Snwhitehorn    d.gauge_start(0, 'percentage: 0', title='Gauge Demo')
342217309Snwhitehorn    for i in range(1, 101):
343217309Snwhitehorn	if i < 50:
344217309Snwhitehorn	    msg = 'percentage: %d' % i
345217309Snwhitehorn	elif i == 50:
346217309Snwhitehorn	    msg = 'Over 50%'
347217309Snwhitehorn	else:
348217309Snwhitehorn	    msg = ''
349217309Snwhitehorn	d.gauge_iterate(i, msg)
350217309Snwhitehorn	sleep(0.1)
351217309Snwhitehorn    d.gauge_stop()
352217309Snwhitehorn    #<>#
353217309Snwhitehorn
354217309Snwhitehorn    d.clear()
355