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