1#!/usr/bin/python 2# $Id: dialog.py,v 1.5 2019/12/10 22:52:52 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 instance of 'Dialog' 256 class named 'd'): 257 d.gauge_start() 258 # do something 259 d.gauge_iterate(10) # passed through 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